Optimizing a Spin-Lock

(david.alvarezrosa.com)

67 points | by signa11 2 days ago

7 comments

  • pizlonator 9 hours ago

    Super dangerous to benchmark lock performance using microbenchmarks. If you have a tiny benchmark, then you're putting the CPU and memory into a very specific and unusual state (everything is quiet other than the lock itself).

    The real world story for locks is usually that you're not rage-contending 100% of the time, but that you have some contention combined with CPUs doing some real work and some real memory accesses.

    What I've found is that in those more real scenarios, the locks that perform best in microbenchmarks fall apart compared to completely different and unexpected algorithms.

    • neeeeees 6 hours ago

      To repurpose a famous quote - all benchmarks are wrong, but some are useful

    • vova_hn2 2 hours ago

      I want to share an excellent related article, "A Concurrency Cost Hierarchy" [0] by Travis Downs [1]. It was posted to HN many times [2], the largest discussion has 26 comments [3].

      My own programming experience is mostly Python, so throughout the most of my career I treated locks as pure magic and didn't think much about what happens under the hood.

      At some point in my life I became interested in Rust and lower-level programming and this article in particular really helped me to set my head straight on this topic. It doesn't only explain how concurrency primitives actually work, but it also explains why they work this way, what choices and trade-offs are involved.

      This article uses C++ for all examples, but there are really nothing language specific, all principles will work in Rust, C, Zig etc

      [0] https://travisdowns.github.io/blog/2020/07/06/concurrency-co...

      [1] https://travisdowns.github.io/

      [2] https://hn.algolia.com/?q=https%3A%2F%2Ftravisdowns.github.i...

      [3] https://news.ycombinator.com/item?id=24489829

      • tombert 12 hours ago

        I genuinely had not heard of anyone actually using a spinlock in production code until I started using LMAX Disruptor a few years ago.

        I was always told that they were an anti-pattern, and I think that generally that is a pretty good rule of thumb, but I guess like most stuff in CS: there are always exceptions to "good rules of thumb".

        I still haven't actually explicitly written a spinlock for anything in production, but Disruptor has shown me that there are cases for it.

        • kazinator 8 hours ago

          Before we had futexes in the Linux kernel, spinlocks were used to boostrap the implementation of everything else in the user space threading library.

          If you have futexes you can try to grab a lock with an atomic operation and if that fails, go wait on the futex via system call, so there is no need to spin. Spinlocks then remain useful as an optimization, because there are situations in which it is cheaper to spin around a bunch of times until the thread on another processor gives up the lock, than to take a trip into the kernel.

          You can also spin, but with a scheduler yield in the loop; we don't normally think of that as a spinlock. That's what you fall back on after spinning some number of times and failing to get the lock.

          In the Linux kernel, spinlocks are the low level primitive. They are very efficient because unlike user space threading, they are not faced with guesswork about scheduling. They are "surgical".

          • bob1029 11 hours ago

            To be really pedantic, it's a spin wait, not a spin lock in disruptor. You are waiting for a sequence, not mutually excluding some resource. Many threads can watch the same volatile at the same time without blocking each other.

            • nly 9 hours ago

              If you have an application where your threads are pinned to dedicated cores, and those cores are all isolated from general OS scheduling, then it's the lowest latency means to synchronize arbitrary things between threads

              Entering the kernel with a futex wait or wake under contention costs a couple of microseconds, whereas a spinlock will cost you double digit to low triple digit nanos depending on cores/sockets etc

              • foldr 24 minutes ago

                One use case I’ve found is for a lock that you don’t need to acquire. For example, you need a lock to read a cache entry, but if you can’t acquire the lock after a few spins, you can just proceed without the cache. For fine-grained locking, a spin lock can have a significantly lower memory overhead than a full futex.

                • BobbyTables2 6 hours ago

                  Tell a kernel developer that spin locks aren’t for production code.

                  Bring a wind turbine with you because the laughing will be quite intense…

                  • BoingBoomTschak 11 hours ago
                    • sedatk 11 hours ago

                      It’s one of the secret ingredients to avoid a Big Kernel Lock™.

                      • ignoramous 11 hours ago

                        > had not heard of anyone actually using a spinlock in production code

                        Go stdlib sync.Mutex uses spins: https://victoriametrics.com/blog/go-sync-mutex / https://archive.vn/BIb7F

                        • mathisfun123 11 hours ago

                          not all architectures have atomic cas

                        • dalvrosa 2 days ago

                          Thanks for sharing! Happy to get feedback :)

                          Note that I don't recommend spinlock for most cases, only when there is a 1:1 mapping between threads and phsycal CPU cores, and only after measuring

                          • loeg 10 hours ago

                            Spinlocks are unsuitable for situations where you can be involuntarily context switched (the vast majority of userspace programs). Probably worth mentioning that.

                          • RossBencina 6 hours ago

                            TFA mentions power usage from a dollar cost perspective, but there is also the thermal aspect. You do not want to trigger thermal throttling (or lose boost) while doing almost nothing.

                            • gavinlilly 9 hours ago

                              If contention is expected, would it be better to first perform a relaxed read before the exchange? For example:

                                auto lock() noexcept -> void {
                                    auto backoff = 1;
                                    do {
                                        while (locked_.load(std::memory_order_relaxed)) {
                                            for (auto i = 0; i < backoff; ++i) _mm_pause();
                                            backoff = backoff < 64 ? backoff << 1 : 64;
                                        }
                                    } while (locked_.exchange(true, std::memory_order_acquire);
                                }
                              • nly 9 hours ago

                                If you're expecting heavy contention, and there's no risk of any of the threads being descheduled, then FIFO spinlocks are probably best.

                                In a FIFO threads register themselves into a linked list, and the thread calling unlock() directly wakes the next. It's possible to have e.g. 20 threads in this case all spinning on their own cache lines (their private node), rather than a shared one (the lock head).

                                This can be coherence protocol optimal.

                                A dumb test and set spinlock, or variant thereof, is going to degrade quickly as all the cores are spinning on the same cacheline causing a lot of coherence traffic between cores (transitions between shared, exclusive and modified states)

                              • jeffbee 11 hours ago

                                This would have different answers depending on if it ran on a machine with a more closely-shared cache, right? For example on an Intel efficiency core cluster where 4 cores share an L2.