상용 하드웨어에서 읽기 위주의 캐시 워크로드에서 원자적 경쟁과 캐시 라인 핑퐁 때문에 RwLock이 Mutex보다 약 5배 느렸던 이유를 살펴본다.
2026-02-21
상용 하드웨어에서, 읽기 위주의 캐시 워크로드에서 원자적(atomic) 경쟁과 캐시 라인 핑퐁 때문에 RwLock이 Mutex보다 약 ~5× 느렸다.
이 글은 “자명한” 최적화가 어떻게 역효과를 낼 수 있는지에 대한 이야기다.
Rust로 고성능 텐서 캐시를 만드는 Redstone를 개발하던 중, 쓰기 락 경합 때문에 벽에 부딪혔다. 읽기 락을 쓰면 아마 완화될 거라고 생각했다(그리고 나는 크게 틀렸다 :( ). 여러 스레드가 동시에 읽을 수 있게 되면 처리량이 치솟을 거라 기대했지만, 결과는 비교가 되지 않았다. 쓰기 락이 읽기 락보다 약 5배 정도 더 빠르게 나왔다.
읽기 락이 왜 더 느려질 수 있는지(그리고 기대를 산산이 부술 수 있는지) 그 이유를 설명해보겠다.
실험은 단순했다. LRU(Least Recently Used) 텐서 캐시를 벤치마킹했다.
parking_lot::RwLock 사용..get() 연산의 처리량(throughput) 최대화.일반적인 LRU 캐시에서 get은 엄밀히 “읽기”가 아니다. 항목을 최근 사용됨으로 표시하기 위해 내부 상태를 업데이트해야 한다. 하지만 조회를 읽기로 취급하고 내부 변경을 근사(approximate)함으로써 처리량을 개선할 수 있는지 보고 싶었다.
표준 문서를 따르면 논리는 그럴듯하다.
.write()): 배타적 접근. 한 번에 하나의 스레드만 진행..read()): 공유 접근. 여러 스레드가 접근 가능.읽기 위주의 텐서 워크로드에서는 RwLock이 큰 승리일 것 같다. 하지만 M4 같은 현대 멀티코어 칩에서는 읽기 락을 “입장”하기 위한 비용이 생각보다 훨씬 크다.

rustpub fn get_with_write(&self, key: &str) -> Option<Arc<Tensor>> { let mut inner = self.inner.write(); inner.get(key) } pub fn get_with_read(&self, key: &str) -> Option<Arc<Tensor>> { let inner = self.inner.read(); if let Some((tensor, _, _)) = inner.map.get(key) { Some(tensor.clone()) } else { None } }
범인은 캐시 라인 핑퐁(Cache Line Ping-Pong)이라 불리는 현상이다.
.read()를 호출하더라도, 하드웨어 수준에서는 쓰기 연산을 수행한다. 현재 락을 잡고 있는 읽기 스레드 수를 추적하기 위해 parking_lot(그리고 다른 모든 RwLock 구현)은 내부 원자 카운터(atomic counter)를 증가시켜야 한다.
현대 CPU는 캐시 라인(Cache Line) 이라 불리는 64바이트 단위로 데이터를 이동시킨다. 코어 1이 리더(reader) 카운트를 증가시키려면, 그 카운터가 들어 있는 캐시 라인에 대한 “배타적(Exclusive)” 소유권을 가져와야 한다.
코어 2가 1나노초 뒤에 락을 읽으려 하면:
캐시 조회처럼 매우 빠른 연산에서는, 스레드들이 텐서를 실제로 찾는 시간보다 리더 카운트 변수의 소유권을 두고 싸우는 시간을 더 많이 쓰게 된다. 캐시 라인이 코어들 사이를 너무 빠르게 튕겨 다니면서 CPU 메모리 버스가 병목이 된다.
역설: 쓰기 락은 하드웨어 버스 관점에서 오히려 더 “덜 시끄럽다”. 동일한 원자 카운터를 여러 코어가 동시에 수정하려고 몰려드는 상황을 막기 때문이다. 대신 단일 스레드에만 제어권을 주고, 그 스레드는 의도한 작업을 수행하고 내부 상태를 수정한 뒤 락을 해제한다.
RwLock의 오버헤드는 거의 항상 동시성의 이점을 상쇄하고도 남는다.perf나 cargo-flamegraph 같은 도구를 사용하라. atomic_add에서 시간이 많이 쓰이는 것을 보면 캐시 경합이 있다는 뜻이다.읽기 락이 전부 나쁘고 악한 것은 아니다. 다음과 같은 경우에는 읽기 락이 훨씬 더 좋다:
Mutex나 더 단순한 락 구현이 오히려 나을 가능성이 크다.락 전략을 사용할 때는 그것을 무엇에 쓰려는지, 사용 패턴이 어떤지 이해하고, 항상 코드를 프로파일링하라. 그러면 이런 이상한 케이스가 눈에 띄게 된다.
Read other posts
λ echo "built with astro. hosted on vercel."