LabHub

컴퓨터 구조 · 메모리 계층 · 이론

캐시 라인과 거짓 공유 — 옆자리 때문에 느려진다

LabHub 에서 이어서 보기

한 줄 요약

캐시는 64바이트 라인 단위로 움직이고 코어들은 그 라인 단위로 소유권을 주고받으므로, 서로 다른 변수라도 같은 라인에 있으면 성능이 서로를 깎는다.

왜 이게 필요했나

멀티코어 시스템에서는 같은 메모리를 여러 코어가 각자의 캐시에 올려 둘 수 있다. 한 코어가 값을 바꿨는데 다른 코어가 옛 값을 계속 본다면 프로그램은 무너진다. 그래서 하드웨어는 캐시 일관성 프로토콜(MESI 계열)을 돌린다. 어떤 코어가 라인을 수정하려면 다른 코어들의 사본을 무효화하고 배타적 소유권을 얻어야 한다.

여기서 중요한 사실은 이 소유권 다툼의 단위가 변수가 아니라 캐시 라인이라는 점이다.

어떻게 동작하나

스레드 A 가 카운터 a 를, 스레드 B 가 카운터 b 를 각각 열심히 증가시킨다고 하자. 둘은 전혀 다른 변수이고 락도 필요 없다. 그런데 ab 가 구조체 안에 나란히 선언되어 같은 64바이트 라인에 들어갔다면, A 가 쓸 때마다 B 의 캐시에서 그 라인이 무효화되고, B 가 쓸 때마다 A 의 것이 무효화된다. 논리적으로는 공유가 없는데 하드웨어 수준에서는 계속 뺏고 뺏기는 상태다. 이것을 거짓 공유(false sharing) 라고 한다.

증상이 특징적이다. 스레드를 늘렸는데 처리량이 늘지 않거나 오히려 줄어든다. 락 경합을 아무리 찾아도 나오지 않는다. 락이 없기 때문이다.

대응은 단순하다. 코어별로 쓰는 데이터를 서로 다른 캐시 라인에 놓는다. 구조체에 패딩을 넣거나, 배열 원소를 라인 크기의 배수로 정렬한다.

비슷한 부류의 함정이 하나 더 있다. 분할 잠금(split lock) 이다. 원자 연산의 피연산자가 두 캐시 라인에 걸쳐 있으면 CPU 는 빠른 일관성 경로를 쓸 수 없어 외부 버스 잠금을 걸어야 한다. 800사이클을 넘고, 그동안 무관한 다른 코어들까지 멈춘다. 리눅스 커널은 이걸 감지하는 기능을 제공하며 부팅 파라미터 split_lock_detect 의 기본값은 warn 이다. 커널 로그에 관련 경고가 찍히고 있다면 실제 성능 문제일 가능성이 높다.

현장에서 만나는 모습

고성능 큐, 카운터 배열, 스레드별 통계 수집기가 전형적인 피해자다. 특히 "스레드마다 자기 슬롯에만 쓰니까 락이 필요 없다"고 설계한 배열이 위험하다. 슬롯 크기가 8바이트라면 여덟 개 슬롯이 한 라인에 들어가고, 여덟 스레드가 한 라인을 놓고 싸운다.

반대로 이 원리를 이용할 수도 있다. 함께 읽히는 필드를 한 라인에 모아 두면 한 번의 미스로 전부 가져온다. 읽기 전용으로 함께 쓰이는 값들은 붙이고, 서로 다른 코어가 쓰는 값들은 떼어 놓는다. 규칙은 이 한 줄로 요약된다.

이어지는 퀴즈에서 확인할 것

락이 없는데도 스레드를 늘릴수록 느려지는 현상을 캐시 라인 소유권으로 설명할 수 있는지 확인한다.