멀티코어 CPU 계층적 캐시 아키텍처와 MESI 프로토콜을 통한 캐시 일관성 확보
현업에서 대규모 트래픽을 다루다 보니 결국 기본기로 돌아가게 되더군요. 교과서적인 지식에 그치지 않고 실제 운영 환경에서 느낀 점을 공유합니다.
현대 프로세서 설계에 있어서 폰 노이만 병목 현상, 즉 메모리 장치와 연산 장치 간의 속도 차이를 해소하는 유일한 방안은 계층화된 고속 캐시 메모리를 두는 것입니다. 멀티코어 환경으로 넘어오면서 코어마다 독립적으로 내장하고 있는 L1, L2 캐시들은 프로그램의 지역성(Locality)을 활용하여 초고속 데이터 접근을 가능하게 합니다. 하지만 동일한 메모리 주소값에 대해 서로 다른 코어가 각자의 캐시에 독자적인 복사본을 유지하게 되면서 캐시 일관성(Cache Coherence) 문제라는 심각한 오류 가능성을 직면하게 되었습니다. 만약 코어 A가 자신이 가지고 있는 캐시 라인의 특정 변수를 조작하고, 코어 B가 메인 메모리나 자신의 캐시에서 갱신되지 않은 이전 값을 읽게 된다면 프로그램의 논리는 붕괴됩니다.
이를 하드웨어 레벨에서 프로그래머 모르게 완벽히 방어해주는 메커니즘이 캐시 일관성 프로토콜이며 가장 대표적인 것이 MESI(Modified, Exclusive, Shared, Invalid) 프로토콜입니다. 이 프로토콜은 각각의 캐시 라인마다 2비트의 상태 플래그를 두어 네 가지 상태 중 하나를 부여합니다. Modified 상태는 해당 코어가 데이터를 수정하였으나 아직 메인 메모리에는 반영되지 않은 더티(Dirty) 상태를 의미하며 다른 캐시에는 이 데이터가 존재할 수 없습니다. Exclusive 상태는 수정되지 않은 깨끗한 데이터이지만 현재 캐시에만 독점적으로 존재함을 나타냅니다. Shared 상태는 여러 코어의 캐시에 동일한 값이 읽기 전용으로 복제되어 있는 상태입니다. 마지막으로 Invalid 상태는 다른 코어가 이 캐시 라인과 동일한 주소의 데이터를 수정함에 따라 더 이상 유효성을 보장할 수 없는 무효화된 상태를 의미합니다.
특정 코어가 Shared 상태의 데이터를 수정하고자 할 때, 프로세서는 버스를 통해 무효화 트랜잭션(Invalidate Transaction)을 브로드캐스팅하여 다른 모든 코어의 해당 캐시 라인을 Invalid 상태로 강제 전환시킵니다. 이후 자신의 상태를 Modified로 변경합니다. 캐시 간에 상태를 통신하고 갱신하는 과정 자체가 비용이 클 뿐만 아니라 두 개 이상의 변수가 우연히 같은 캐시 라인에 맵핑되어 있어 무관한 변수를 다른 스레드가 갱신할 때도 나의 캐시가 무효화되는 거짓 공유(False Sharing) 현상이 발생합니다. 무잠금 병렬 프로그래밍에서는 이런 False Sharing을 방지하기 위해 변수들 사이에 캐시 라인 크기만큼의 패딩을 주입하는 기법을 널리 사용합니다. 이는 오로지 하드웨어의 미세한 동작 원리를 이해해야만 최적화할 수 있는 영역입니다.