Genuine questions, because I really don't know.
The textbook example of a spinlock is maybe a synchronized counter (iterations++), or maybe a producer/consumer queue.
- invokes the accept() function of the socket family: https://github.com/torvalds/linux/blob/be779f03d563981c65cc7...
- which invokes the accept() function of the protocol: https://github.com/torvalds/linux/blob/be779f03d563981c65cc7...
- which invokes lock_sock_nested(), which spins https://github.com/torvalds/linux/blob/be779f03d563981c65cc7...
Yes, there are spin-locks involved in the process. But I'm talking about these lines of code:
> error = inet_csk_wait_for_connect(sk, timeo);
> mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);
Etc. etc.
You know, the stuff that causes milliseconds to multiple-seconds worth of delay, as opposed to "pause / spinlocks" which is measured in nanoseconds.
You can find similar behaviour in the filesystem APIs, anything touching the VM (mmap_sem IIRC is also a spinlock), pretty much any OS API where threads are all banging at the same shared resource that isn't expected to require a long wait. struct file also contains a spinlock, but doesn't look like it's used in normal operation
Hmmm... okay. I think I see what you're going for. I think its a bit of a muddy example though because of the mutex.
But pre-mutex, there's definitely a spinlock, and the 40-cores would definitely hit that first. Mutexes themselves are likely a spinlock as well, so there's a chance (a low-chance... but a chance nonetheless) that everything is fast-pathed and never sleeps a thread.
So yeah, there are better examples you could use. But upon further analysis, it does seem like your example does work with the right frame of mind.
https://www.youtube.com/watch?v=9hJkWwHDDxs
So actually, you're about right. Its "wrong" to spinlock above 40 cores. But at 40-cores or less, you probably should prefer spinlocks over atomics or other primitives on the x86 platform.
As for what? Well, you need a synchronization primitive for a producer-consumer queue. A spinlock would be a good choice for that, even on 40+ core machines (since most of those cores wouldn't be spinning at the same time, even if they're all locking the same lock).
Remember, x86 primitive for synchronization is the "LOCK" prefix. When an x86 core "LOCK"s an instruction, no other core is allowed to touch the data its touching. (In MESI protocol: the cache line goes into the "exclusive" state). As such, spinlocks match the fundamental nature of the x86 instruction set very very well.
Can anybody actually explain this result? The spinlock needs at least one atomic operation and a write (for lock + unlock) and one non-atomic read-modify-write per iteration while the atomic increment needs only a single atomic operation per iteration and no other memory access. How on Earth can the latter variant be slower?
The only thing I can think of right now is that due to some micro-architectural idiosyncracies, the relevant cache lines end up migrating between cores more often in the atomic increment case, while in the spinlock case, the same core ends up winning the lock back-to-back more often (and so cachelines migrate less frequently which reduces the overall running time).
That seems like it would most likely only ever be relevant to this kind of extremely micro benchmark, but I'd be interested to hear if there are other explanations (I haven't watched the whole talk, but at least the slides don't seem to make an attempt to explain this).
Furthermore, ALL loads / stores are almost strictly ordered on x86.
http://www.cs.cmu.edu/~410-f10/doc/Intel_Reordering_318147.p...
> Intel 64 memory ordering obeys the following principles:
> 1. Loads are not reordered with other loads.
> 2. Stores are not reordered with other stores.
> 3. Stores are not reordered with older loads.
> 4. Loads may be reordered with older stores to different locations but not with older stores to the same location.
> 5. In a multiprocessor system, memory ordering obeys causality (memory ordering respects transitive visibility).
> 6. In a multiprocessor system, stores to the same location have a total order.
> 7. In a multiprocessor system, locked instructions have a total order.
> 8. Loads and stores are not reordered with locked instructions.
As such, "relaxed atomics" DO NOT EXIST on x86. Almost everything on x86 is innately an acquire / release semantics (even non-locked stuff).
------------
As such, the optimal spinlock-unlock implementation on x86 doesn't use the LOCK prefix at all! You rely upon the memory-ordering consistently guaranteed by the x86 processor, and let the cache-coherence mechanism handle everything for you.
You'll still need the "lock" to obtain the lock (writing 1 into the memory location AND reading the old value). But unlocking the lock (writing 0 into the memory location) can be done x86-LOCK-prefix free.
In contrast, a CAS-atomic on x86 needs to "LOCK" the memory bus, which under x86 is specified as:
> Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signal is asserted.
So when you do a CAS-atomic on x86, in REALITY, the LOCK-prefix will be asserted, locking out all other cores from interacting with your cache line.
------------
So the cache-coherence mechanism on x86 seems sufficiently robust and well optimized by Intel / AMD, at least with our current 28-core Xeons and 32-core EPYCS. Future scaling may be an issue for Intel/AMD, and ARM / Power9 (which have true relaxed memory) may scale better eventually.
> As such, the optimal spinlock implementation on x86 doesn't use the LOCK prefix at all! You rely upon the memory-ordering consistently guaranteed by the x86 processor, and let the cache-coherence mechanism handle everything for you.
That's just not true. Spinlocks are not magic; they need to be implemented, and all the implementations I know of use a (atomic and therefore locked!) cmpxchg.
Which makes sense. Spinlocks are a synchronization primitive, and you wrote yourself that ALL synchronization primitives on x86 are "LOCK" prefixed.
So let's rephrase the original question: why should a locked cmpxchg + additional memory accesses be more efficient than a single locked increment?
> So let's rephrase the original question: why should a locked cmpxchg + additional memory accesses be more efficient than a single locked increment?
The ideal spinlock on x86 isn't a cmpxchg btw. Its a pure locked-swap.
And I think there-in lies the answer. A locked-swap on x86 can theoretically execute faster (its a pure write and a pure read to the cache), while the uOps generated for a cmpxchg would include a comparison.
From a cache-coherency perspective: the pure-write of a locked-swap can be implemented by invalidating the caches of every other core. So there's a "race" between cores to send out the "invalidate cache" message to everybody. Everyone who loses to the invalidate-message needs to stall and re-read the new value.
But otherwise, the "winner" of locked-swap executes incredibly efficiently. The winner instantly transitions into the "Owner" state and continues to execute.
But a cmpxchg is a far more complicated order of events. I'd imagine that the underlying, undocumented, cache-coherence mechanisms struggle under a LOCKed cmpxchg (which is far more common with atomic-based lockless programming).
Remember, the "LOCK" prefix is implemented by the MESI protocol (or something more complicated, like MESIF on Intel or MEOSI on AMD) in reality. Even then, modern processors probably implement something far more complicated, and its unfortunate that its undocumented.
Still, LOCK swap is clearly more efficient under MESI than LOCK cmpxchg.
---------
A locked-increment would probably be roughly the same speed as a cmpxchg btw. Both a locked-increment and locked-cmpxchg require a full read/modify/write cycle.
While a locked-swap is just a read / invalidate+write instead.
For various reasons I still suspect that some other factor is at play, but I also don't really know how to tease that out. Maybe there are some performance counters one could look at, but I'm just speculating.
On the other hand, if the spin lock is contended, avoiding an unconditionally acquiring the cacheline in exclusive mode is beneficial as it will mitigate cacheline ping pong. Thus the test-and-test-and-set idiom.
On the third hand,if the lock is contended, a spin lock is probably inappropriate...
Also I want to mention that CAS can fail, while xchg always succeeds, but the performance implications are not clear cut: for example a spin lock will issue xchg in a loop (failure is encoded at higher level), while wait free algos can issue cas outside of loops as a failed cas implies that another cas succeded and thus carries information.
You almost had a perfect post IMO :-) But this last line is incorrect on some systems.
x86 CAS seems to be strong as you imply. But apparently, Power9 or ARM CAS can have spurious failures. Or more appropriately, Power9 and ARM implement "Load Linked / Store Conditional", which can have spurious failures, and thus compare-and-swap (at a higher-level, like in C++ or C) will look like a CAS failed spuriously.
Which is why atomic_compare_exchange_weak and atomic_compare_exchange_strong exists. Weak doesn't "hold information" due to the potential of spurious failures, but can be a more efficient implementation in the case of LL/SC instructions.
http://liblfds.org/mediawiki/index.php?title=Article:CAS_and...
Still (but don't quote me on this because I'm no Power expert), IIRC Power actually does guarantee, via idiom recognition, that specific uses of LL/SC are wait free by downgrading it internally to an actual CAS after a number of failures, so a compare_exchange_strong which loops a bounded number of times can be implemented on this architecture.
I'm okay with your conclusion, but your first sentence is incorrect. The XCHG instruction is fully atomic and sequentially consistent even without a LOCK prefix. (This isn't to say x86 has any particular magic -- XCHG with a memory operand is just automatically LOCKed.) Also, ordinary stores are releases, and are thus quite useful as synchronization primitives, without any LOCK prefix at all.
Atomic RMWs on intel (and really the vast majority of high performance architectures) have no effect outside of the core issuing them except in some very exhotic circumstances. The Lock signal is a relic of the pre-P6 past when everything communicated via a single bus.
What prevents a cache line from being 'interacted' with, is being held in exclusive mode. This is a property of any write, atomic or not. An RMW won't hold a cacheline significantly longer than a normal write would.
https://www.youtube.com/watch?v=9hJkWwHDDxs
Atomics-based lock-free data-structures scale beyond 40+ core counts. But note that x86's "lock" instruction is fundamental to the system. x86 doesn't have relaxed atomics or barriers under most circumstances. As such, the spinlock best represents what the x86 instruction set offers.