본문으로 건너뛰기
Embedded Performance Engineering · 33/57

Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS

· Hawk · 5분 읽기

#한 줄 요약

“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 miss
MCS : 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 core
hold 100 ns 0 ns 30 ns 90 ns
hold 1 µs 0 ns 400 ns 2 µs
hold 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_relaxYIELD 힌트는 spin loop의 표준 관용구입니다.

다음 편은 Mutex 성능 — futex와 priority inheritance를 분석합니다.

#관련 항목

Embedded Performance Engineering · 34 of 57

  1. 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
  2. 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
  3. 3성능 지표 정의 — Latency·Throughput·Utilization 분석
  4. 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
  5. 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
  6. 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
  7. 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
  8. 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
  9. 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
  10. 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
  11. 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
  12. 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
  13. 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
  14. 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
  15. 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
  16. 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
  17. 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
  18. 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
  19. 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
  20. 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
  21. 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
  22. 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
  23. 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
  24. 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
  25. 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
  26. 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
  27. 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
  28. 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
  29. 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
  30. 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
  31. 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
  32. 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
  33. 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
  34. 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
  35. 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
  36. 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
  37. 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
  38. 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
  39. 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
  40. 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
  41. 41Linux perf 기초 — stat·record·report 활용
  42. 42Linux perf 고급 — Raw Event·Tracepoint·perf script
  43. 43ftrace 활용 — function·function_graph·latency tracer
  44. 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
  45. 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
  46. 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
  47. 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
  48. 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
  49. 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
  50. 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
  51. 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
  52. 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
  53. 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
  54. 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
  55. 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
  56. 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
  57. 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복