Optimizing a Spin-Lock
스핀락 최적화 — 가장 단순한 락에서 피코초까지 쥐어짜기
atomic exchange 루프로 만든 순진한 스핀락을 acquire/release 메모리 순서, test-and-test-and-set, 지수 백오프 순으로 개선해 4스레드 기준 246ns에서 43.0ns로 5.7배 빠르게 하고 에너지 소비도 64.92J에서 11.92J로 5.4배 줄이는 단계별 최적화 과정을 다룹니다. 캐시 라인 소유권 경쟁과 분기 예측 실패가 성능 저하의 원인임을 perf 수치로 보여주며, 저자는 std::mutex가 여전히 올바른 기본값이라고 강조합니다.
- 주제
본문
스핀락은 잠들지 않는 락입니다. 스케줄러에 양보하는 대신 스레드가 CPU 위에서 계속 회전하므로 시스템 콜도 컨텍스트 스위치도 없습니다. 저자 David Álvarez Rosa는 가장 단순한 형태의 스핀락에서 출발해 네 단계의 최적화를 거치며 최종적으로 5.7배 빠르고 에너지는 5.4배 적게 쓰는 버전을 만들어가는 과정을 보여줍니다.
벤치마크 설정
Google Benchmark 기반 벤치마크에서 스레드들은 락을 잡고 공유 카운터를 증가시킵니다. 락과 카운터는 각각 alignas(std::hardware_destructive_interference_size)로 별도 캐시 라인에 배치되고, 스레드는 코어에 고정됩니다. clang으로 모든 최적화를 켜고 빌드했습니다.
V1: 순진한 스핀락
atomic_bool 하나와 exchange 루프로 구성됩니다. exchange는 원자적으로 true를 쓰고 이전 값을 반환하는데, false면 락이 비어 있었다는 뜻이므로 획득에 성공하고 true면 재시도합니다. 경쟁이 없을 때는 3.14ns지만 스레드 2개에서 61.5ns, 4개에서 246ns로 20배 이상 늘어납니다. 코어가 캐시 라인을 독점 소유해야만 쓸 수 있으므로 대기자들이 서로 라인을 빼앗게 되고, L1-d 미스가 1.27%에서 61.73%로 폭증하며 분기 8개 중 1개가 미스예측됩니다. exchange 성공 여부가 다른 코어에 의해 결정되므로 분기 예측기가 학습할 것이 없기 때문입니다. 4스레드에서 에너지는 64.92J를 소비합니다. RAPL 카운터는 시스템 전체 모드와 root 권한이 필요하므로 이 수치는 유휴 코어를 포함한 패키지 전체 기준입니다. 스핀은 에너지를 쓰며, 거래소 코로케이션이 전력을 과금하고 NYSE가 32kW 상한을 두는 고빈도 거래 업계에서는 실제 비용 문제가 됩니다.
V2: 메모리 순서
기본값인 seq_cst는 락에 필요한 것보다 강합니다. 진입 시 acquire, 해제 시 release면 충분합니다. x86에서는 lock()의 xchg가 어차피 locked 연산이라 생성 코드가 동일하지만, unlock()이 두 번째 locked read-modify-write에서 plain store로 바뀝니다. 원자 연산이 두 개에서 하나로 줄어 무경쟁 1.57ns, 4스레드 131ns가 되고 L1-d 미스는 21.16%, 분기 미스는 7.43%, 에너지는 34.45J로 감소합니다.
V3: Test-and-test-and-set
exchange는 실패해도 라인에 쓰기를 하므로 대기자는 쓰기를 멈춰야 합니다. 한 번 exchange한 뒤에는 읽기 전용 load로 회전하고 _mm_pause로 코어를 쉬게 합니다. 실패하는 읽기는 relaxed여도 되는데, 임계 구역의 순서를 정하는 것은 성공하는 exchange이지 실패하는 읽기가 아니기 때문입니다. 2스레드가 32.5ns에서 21.3ns로 3분의 1 줄고, 4스레드는 120ns, L1-d 미스 17.31%, 분기 미스 3.72%, 에너지 30.97J가 됩니다. 읽기 전용 회전은 예측 가능합니다.
V4: 지수 백오프
모든 대기자가 같은 시간 동안 pause하면 동시에 깨어나 다시 충돌합니다. Intel Optimization Reference Manual의 Example 2-10(Contended Locks with Increasing Back-off)이 제시하는 해법대로, 매 라운드 대기 시간을 상한 64까지 두 배씩 늘리면 대기자들이 서로 다른 간격으로 깨어나게 됩니다. 그 결과 4스레드가 120ns에서 43.0ns로 떨어지고 L1-d 미스는 12.88%, 에너지는 11.92J로 순진한 버전 대비 5.4배 절감됩니다.
결론
저자는 대부분의 코드에서 std::mutex가 여전히 올바른 기본값이라고 정리합니다. 스핀락은 스레드가 전용 코어에 고정된 경우에만, 그리고 반드시 측정한 뒤에 고려해야 하며, writer 하나에 reader 다수라면 seqlock이 대안이 될 수 있습니다.
Hacker News 반응
- @dalvrosa — 글 작성자로, 대부분의 경우 스핀락을 권하지 않으며 스레드와 물리 CPU 코어가 1:1로 대응하는 경우에만, 그리고 측정 후에 사용하라고 다시 강조합니다.
- @loeg — 비자발적 컨텍스트 스위치가 발생할 수 있는 환경, 즉 대부분의 유저스페이스 프로그램에는 스핀락이 부적합하며 언급할 가치가 있다고 지적합니다.
- @tombert — LMAX Disruptor를 쓰기 전까지 프로덕션 코드에서 스핀락을 실제로 쓰는 사례를 들어본 적이 없었고 안티패턴으로 배웠지만, 좋은 경험 법칙에도 항상 예외가 있다고 말합니다.
- @sedatk — Big Kernel Lock을 피하기 위한 비밀 재료 중 하나라고 평합니다.
- @BoingBoomTschak — Linus Torvalds의 관련 발언이 주제를 잘 짚는다며 링크를 인용합니다.
원문: Hacker News / 번역·요약: Trawling