// HACKER NEWS — CYBERSECURITY
Optimizing a Spin-Lock
A spin-lock is a lock that never sleeps. Instead of yielding to the
scheduler, the thread stays on the CPU and spins. No syscalls. No
context switches. In this post, we’ll build a version, step by step,
that is 5.7x faster while drawing 5.4x less energy.
Threads increment a shared counter under the lock.1 1
Run on a box
tuned for benchmarking. Built with clang. All optimizations
enabled.
The lock and the counter get a cache line each. Threads are pinned.
An atomic bool and an exchange loop.2 2
exchange atomically writes
true and returns the previous value. false means the lock was free
and is now ours. true means someone else holds it, so we retry.
Uncontended it takes 3.14 ns. Two threads take 61.5 ns, twenty times as
long. Four take 246 ns.
A core must own the line exclusively to write it, so waiters take it
from each other. L1-d misses go from 1.27% at one thread to 61.73% at
four, and one branch in eight is mispredicted.3 3
Whether the exchange
succeeds is decided by the other cores, so the branch predictor has
nothing to learn.
Spinning costs energy.4 4
High-frequency trading shops care about it.
Exchange colocation services charge for power, and NYSE caps at 32 kW.
At
four threads it draws 64.92 J.5 5
Reading the RAPL counters requires
system-wide mode (-a) and root, so the figure covers the whole
package, idle cores included.
The default is seq_cst, stronger than a lock needs. It only has to
acquire on the way in and release on the way out.
The difference is in unlock. The default ordering adds a second
locked read-modify-write, on top of the one in lock.
With memory_order_release, unlock is a plain store.