Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
#한 줄 요약
“Spinlock은 busy wait이며 hold time이 context switch 비용보다 짧을 때만 의미가 있습니다.”
#어떤 문제를 푸는가
Mutex처럼 block 가능한 lock은 context switch가 동반됩니다. Context switch는 Cortex-A에서 1-3 µs, x86에서 1-5 µs 정도가 들기 때문에, critical section이 100 ns 짜리라면 lock 자체보다 switch 비용이 훨씬 큽니다.
Spinlock은 lock이 풀릴 때까지 CPU를 점유한 채 계속 atomic read를 돌립니다. CPU가 낭비되지만 context switch가 없으므로, 짧은 critical section에서는 훨씬 빠릅니다. 단, 잘못 쓰면 CPU 100%를 그대로 태우면서 진척이 없게 됩니다.
이 글에서는 spinlock의 기본 구현부터 ticket lock, MCS lock까지 scalability가 어떻게 달라지는지 살펴봅니다. SMP가 늘어날수록 단순 spinlock은 cache coherence traffic으로 무너지기 시작합니다.
#Spin vs Sleep — 손익 분기
Hold time 1 µs: Spin: 1 µs CPU 낭비, 곧바로 진행 Sleep: context switch ~3 µs × 2 = 6 µs overhead → Spin이 우세
Hold time 1 ms: Spin: 1 ms × N CPU 낭비 Sleep: 6 µs overhead → Sleep이 우세경험칙은 hold time이 context switch 비용의 두 배 이하면 spinlock을 씁니다. 이보다 길면 mutex가 시스템 전체 효율에서 유리합니다.
#기본 Spinlock — Test-and-Set
typedef struct { atomic_int locked; } spinlock_t;
void spin_lock(spinlock_t *lock) { while (atomic_exchange(&lock->locked, 1)) { /* spin */ }}
void spin_unlock(spinlock_t *lock) { atomic_store(&lock->locked, 0);}ARM에서는 LDREX/STREX 쌍으로 구현합니다.
spin: ldrex r1, [r0] cmp r1, #0 bne spin mov r2, #1 strex r3, r2, [r0] cmp r3, #0 bne spin문제는 모든 코어가 같은 cache line에 write를 시도하면서 cache coherence traffic이 폭발한다는 점입니다. 8-core에서 한 lock에 경쟁하면 bus가 invalidate 메시지로 가득 차게 됩니다.
#Test-and-Test-and-Set (TTAS)
void spin_lock(spinlock_t *lock) { while (1) { while (atomic_load_explicit(&lock->locked, memory_order_relaxed)) { cpu_relax(); } if (!atomic_exchange(&lock->locked, 1)) return; }}내부 spin은 read-only이므로 cache line이 Shared 상태로 유지됩니다. 모든 코어가 read만 하면 bus traffic은 0에 가깝습니다. Lock이 풀린 순간에만 atomic_exchange가 호출되어 한 번의 invalidate가 발생합니다.
대부분의 실용적 spinlock 구현이 TTAS를 기본으로 합니다.
#Ticket Lock — FIFO Fair
typedef struct { atomic_int next; atomic_int now_serving;} ticket_lock_t;
void ticket_lock(ticket_lock_t *l) { int my_ticket = atomic_fetch_add(&l->next, 1); while (atomic_load(&l->now_serving) != my_ticket) { cpu_relax(); }}
void ticket_unlock(ticket_lock_t *l) { atomic_fetch_add(&l->now_serving, 1);}도착 순서대로 ticket을 받고 자기 번호가 호출될 때까지 기다리므로 FIFO가 보장됩니다. Starvation이 없는 것이 장점입니다.
단점은 모든 waiter가 같은 now_serving cache line을 보고 있다는 점입니다. Unlock 한 번에 N개 코어 모두에서 invalidate가 발생합니다.
#MCS Lock — Scalable
struct mcs_node { struct mcs_node *next; atomic_int locked;};
void mcs_lock(struct mcs_node *l, struct mcs_node *self) { self->next = NULL; self->locked = 1;
struct mcs_node *prev = atomic_exchange(l, self); if (prev) { prev->next = self; while (atomic_load(&self->locked)) cpu_relax(); }}
void mcs_unlock(struct mcs_node *l, struct mcs_node *self) { if (!self->next) { if (atomic_compare_exchange(l, &self, NULL)) return; while (!self->next) cpu_relax(); } atomic_store(&self->next->locked, 0);}각 waiter가 자기 mcs_node->locked만 spin합니다. Cache line이 코어별로 분리되어 있으므로 bus traffic이 0이 됩니다. Unlock은 다음 노드의 locked를 0으로 쓰는 한 번의 store만 발생합니다.
Linux kernel의 qspinlock이 이 아이디어를 발전시킨 구현으로, 4.2 이후 표준 spinlock으로 자리 잡았습니다.
#Cache Line Bouncing 비교
TTAS : N waiter — unlock 시 N CPU 모두 invalidate, 1 CPU만 성공Ticket : N waiter — 모두 now_serving 공유 → 매 unlock N missMCS : N waiter — 각자 다른 line → unlock 시 1 line만 invalidate대규모 SMP에서는 차이가 극적입니다. 32-core 이상에서 TTAS는 throughput이 거의 0으로 무너지지만, MCS는 거의 일정한 비용으로 유지됩니다.
#Linux Kernel — spin_lock
spin_lock(&mylock);critical_code();spin_unlock(&mylock);
unsigned long flags;spin_lock_irqsave(&mylock, flags);critical_code();spin_unlock_irqrestore(&mylock, flags);spin_lock_irqsave는 IRQ를 끄고 spin하므로 ISR과 task가 같은 lock을 안전하게 공유할 수 있습니다. 같은 코어에서 ISR이 lock을 다시 잡으려고 시도해도 deadlock에 빠지지 않습니다.
#Cortex-M에서의 Spinlock
Cortex-M3/M4 single core: spinlock은 의미가 없습니다. 다른 task가 lock을 풀려면 CPU가 필요한데 spin이 CPU를 점유하므로 영원히 풀리지 않습니다. → IRQ disable 또는 BASEPRI로 critical section을 보호합니다.
SMP Cortex-M (RP2040, Cortex-M55+M85): cross-core spinlock이 필요합니다. CMSIS RTOS와 Zephyr가 atomic API를 제공합니다.Zephyr SMP의 사용 예입니다.
k_spinlock_t lock;k_spinlock_key_t key = k_spin_lock(&lock);critical_code();k_spin_unlock(&lock, key);key는 spin 진입 시점의 IRQ 상태를 저장해 unlock 때 복원합니다.
#Adaptive Mutex — Spin 후 Sleep
void adaptive_lock(lock_t *l) { int spins = 0; while (!try_lock(l)) { if (++spins > THRESHOLD) { block_self(); return; } cpu_relax(); }}수십 µs 정도 spin해 보고 그래도 안 풀리면 sleep으로 전환합니다. 짧은 contention은 spin으로, 긴 것은 mutex로 자동 분기되므로 두 방식의 장점을 모두 얻습니다. Linux mutex의 기본 동작이며 Solaris adaptive mutex도 같은 방식입니다.
#ARM YIELD 힌트
static inline void cpu_relax(void) { asm volatile ("yield");}YIELD 명령은 SMT thread에 자원을 양보하고, 일부 코어에서는 저전력 상태로 잠시 진입합니다. Spinlock loop의 표준 관용구입니다.
x86에서는 PAUSE, RISC-V에서는 Zihintpause 확장의 pause가 같은 역할을 합니다.
#자주 보는 함정과 안티패턴
⚠️ Single-core에서 spinlock
/* Cortex-M3 — 단일 core */spin_lock(&l); /* 영원 spin */다른 task가 lock을 풀어야 하는데 CPU를 양보할 수 없으므로 시스템이 멈춥니다. Mutex나 IRQ disable을 써야 합니다.
⚠️ Long hold time spinlock
spin_lock(&l);http_request(); /* 수 초 동안 모든 다른 core가 spin */spin_unlock(&l);Network I/O처럼 비결정적 작업은 spinlock 안에 두면 안 됩니다. Mutex로 전환해야 합니다.
⚠️ IRQ enable 상태에서 spinlock
spin_lock(&l);/* IRQ 활성 → ISR이 같은 lock 시도 → deadlock */spin_lock_irqsave를 써야 ISR과 안전하게 공유할 수 있습니다.
⚠️ cpu_relax 없는 spin
while (locked) ; /* hot loop, power 낭비, SMT 굶주림 */cpu_relax() 또는 __asm__("yield")를 반드시 넣어야 SMT thread와 전력 절감이 동작합니다.
#측정 — 실측 결과
Cortex-A72 4-core에서 hold time을 바꿔 가며 측정한 평균 wait time입니다.
1 core 2 core 4 corehold 100 ns 0 ns 30 ns 90 nshold 1 µs 0 ns 400 ns 2 µshold 10 µs 0 ns 8 µs 25 µs ← mutex 우세 시작hold 100 µs 0 ns 80 µs 250 µs ← mutex 압도적Hold time이 10 µs 부근부터 mutex가 더 유리해지는 경계가 보입니다. 이 측정값을 기준으로 spinlock과 mutex를 선택하는 가이드라인을 세울 수 있습니다.
#정리
- Spinlock은 busy wait이며 hold time이 짧을 때만 의미가 있습니다.
- TTAS, ticket, MCS 순으로 scalability가 개선됩니다.
- 대규모 SMP에서는 qspinlock이나 MCS lock이 필수입니다.
- Cortex-M single core에서는 spinlock이 무의미하며 IRQ disable로 대체합니다.
- Adaptive lock은 spin 후 sleep으로 두 방식의 장점을 모두 활용합니다.
cpu_relax와YIELD힌트는 spin loop의 표준 관용구입니다.
다음 편은 Mutex 성능 — futex와 priority inheritance를 분석합니다.
#관련 항목
Embedded Performance Engineering · 34 of 57
- 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
- 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
- 3성능 지표 정의 — Latency·Throughput·Utilization 분석
- 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
- 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
- 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
- 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
- 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
- 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
- 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
- 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
- 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
- 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
- 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
- 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
- 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
- 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
- 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
- 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
- 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
- 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
- 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
- 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
- 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
- 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
- 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
- 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
- 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
- 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
- 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
- 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
- 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
- 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
- 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
- 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
- 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
- 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
- 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
- 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
- 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
- 41Linux perf 기초 — stat·record·report 활용
- 42Linux perf 고급 — Raw Event·Tracepoint·perf script
- 43ftrace 활용 — function·function_graph·latency tracer
- 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
- 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
- 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
- 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
- 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
- 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
- 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
- 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
- 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
- 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
- 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
- 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
- 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
- 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
관련 글
SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
Per-core utilization과 CPU affinity, NUMA, migration cost, Amdahl 한계.
SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
ARM LDREX/STREX exclusive monitor와 ARMv8.1 LSE를 출발점으로 SMP spinlock 구현을 따라갑니다. test-and-test-and-set, ticket lock, MCS lock의 fairness와 cache bouncing trade-off, WFE/SEV로 만드는 저전력 spin을 정리합니다.
실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
70B 모델 KV cache가 HBM 한계를 넘어 throughput이 무너졌을 때, CXL.mem 256 GB pool 추가로 회복한 실전 케이스.
이 글을 참조하는 글 (4)
- SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV— Practical RTOS Internals
- Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance— Embedded Performance Engineering
- Lock Contention 분석 — Wait·Hold·Convoy·측정 기법— Embedded Performance Engineering
- Spinlock vs Mutex 결정 가이드 — Context Switch·Hold Time— Modern Embedded Recipes