Adaptive Spin Count Mechanism in Abseil SpinLock: Implementation and Configuration
Abseil's SpinLock implements an adaptive spin count mechanism that dynamically calibrates busy-wait iterations based on CPU topology, defaulting to 1000 spins on multi-core systems and 1 spin on single-core machines, with runtime tunability via static accessor methods.
The SpinLock class in the abseil/abseil-cpp repository provides a lightweight mutual exclusion primitive optimized for low-contention scenarios. Its adaptive spin count mechanism determines how long a thread should busy-wait before yielding, balancing acquisition latency against CPU consumption. This system automatically initializes based on hardware characteristics but remains fully configurable for specific workload requirements.
How the Adaptive Spin Count Works
The mechanism centers on a static atomic variable adaptive_spin_count_ that controls the maximum number of spin iterations before the lock falls back to a more expensive waiting strategy.
Lazy Initialization on First Use
In absl/base/internal/spinlock.cc, the first time a thread enters the slow path via SpinLoop(), the count initializes based on the number of logical CPUs:
// In SpinLoop()
if (adaptive_spin_count_.load(std::memory_order_relaxed) == 0) {
int current_spin_count = 0;
int new_spin_count = NumCPUs() > 1 ? 1000 : 1;
adaptive_spin_count_.compare_exchange_weak(
current_spin_count, new_spin_count,
std::memory_order_relaxed, std::memory_order_relaxed);
}
- Multi-core systems: Default is 1000 iterations, allowing threads to spin briefly while expecting the lock holder to release soon on another core.
- Single-core systems: Default is 1 iteration, preventing wasteful CPU cycles since the holder cannot progress while the spinner consumes the CPU.
The Spin Loop Implementation
After initialization, SpinLoop spins up to adaptive_spin_count_ times, checking the lock word each iteration before yielding:
int c = adaptive_spin_count_.load(std::memory_order_relaxed);
do {
lock_value = lockword_.load(std::memory_order_relaxed);
} while ((lock_value & kSpinLockHeld) != 0 && --c > 0);
This loop uses std::memory_order_relaxed for minimal overhead during the busy-wait phase, as precise ordering is not required until the lock is actually acquired.
Runtime Configuration
The adaptive spin count remains fully configurable after initialization through public static methods declared in absl/base/internal/spinlock.h:
GetAdaptiveSpinCount(): Returns the current atomic valueSetAdaptiveSpinCount(int): Writes a new value to the atomic counter
This allows applications to tune contention behavior dynamically—reducing the count to minimize CPU waste under heavy contention, or increasing it for ultra-low latency in scenarios with brief critical sections.
Implementation Details in spinlock.cc
The core logic resides in absl/base/internal/spinlock.cc within the SpinLoop method. The implementation uses compare_exchange_weak to ensure thread-safe initialization without requiring static initialization order guarantees, avoiding the static initialization order fiasco.
The check against kSpinLockHeld in the loop condition ensures spinning stops immediately if the lock becomes available, regardless of the remaining count.
Practical Usage Example
#include "absl/base/internal/spinlock.h"
void Example() {
// Default spin count (auto-initialized based on CPUs)
absl::base_internal::SpinLock lock;
{
absl::base_internal::SpinLockHolder holder(&lock);
// critical section …
}
// Custom configuration: 200 iterations before yielding
absl::base_internal::SpinLock::SetAdaptiveSpinCount(200);
int current = absl::base_internal::SpinLock::GetAdaptiveSpinCount(); // returns 200
}
Summary
- Adaptive spin count defaults to 1000 iterations on multi-core systems and 1 on single-core systems, calibrated automatically in
SpinLoop(). - The implementation in
absl/base/internal/spinlock.ccuses lazy initialization viacompare_exchange_weakto avoid static initialization ordering issues. - Runtime configuration is available through
GetAdaptiveSpinCount()andSetAdaptiveSpinCount()static methods. - The spin loop uses
std::memory_order_relaxedmemory ordering for minimal overhead during busy-waiting. - This mechanism balances low acquisition latency against CPU utilization by limiting wasteful spinning when contention is high.
Frequently Asked Questions
What is the default adaptive spin count in Abseil SpinLock?
On multi-core machines, the default is 1000 spin iterations. On single-core machines, the default is 1 iteration. This calibration occurs automatically the first time a thread enters the SpinLoop method in absl/base/internal/spinlock.cc.
How does SpinLock detect whether to use 1000 or 1 spin iterations?
The code calls NumCPUs() during lazy initialization and sets the value to 1000 if NumCPUs() > 1, otherwise 1. This check happens inside compare_exchange_weak in SpinLoop() to ensure thread-safe initialization.
Can I change the spin count at runtime?
Yes. The SpinLock class provides static methods GetAdaptiveSpinCount() and SetAdaptiveSpinCount(int) declared in absl/base/internal/spinlock.h. These allow reading and modifying the atomic adaptive_spin_count_ variable to tune performance for specific workloads.
Why does single-core mode default to only 1 spin iteration?
On a single-core system, the thread holding the lock cannot execute while another thread spins, making prolonged spinning entirely wasteful. The default of 1 iteration ensures the waiter yields immediately, allowing the holder to progress and release the lock.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →