캐시 일관성
더 많은 작업
캐시 일관성(Cache Coherence)은 여러 프로세서나 코어가 각자의 캐시에 같은 메모리 블록의 복사본을 가지고 있을 때, 어느 한쪽이 그 값을 바꾸면 다른 모든 복사본이 그 변경을 일관되게 보도록 보장하는 성질이다.
캐시 메모리는 주기억장치의 일부를 복사해 두고 쓰기 때문에, 복사본이 둘 이상 생기는 순간 값이 서로 어긋날 수 있다. 단일 프로세서에서는 캐시가 하나뿐이라 쓰기 정책만 맞추면 되지만, 코어마다 L1·L2 캐시를 따로 가진 멀티코어 프로세서나 여러 CPU가 메모리를 공유하는 대칭형 다중 처리(SMP) 시스템에서는 코어 A가 자기 캐시의 값을 바꿔도 코어 B의 캐시에는 옛 값이 남는다. 이 상태로 B가 읽으면 낡은 데이터(stale data)를 쓰게 되고, 공유 변수로 동기화하는 프로그램은 틀린 결과를 낸다.
캐시 일관성 프로토콜은 이런 어긋남을 하드웨어 수준에서 막는다. 오늘날 범용 멀티코어 CPU는 모두 하드웨어 캐시 일관성을 갖추고 있어서, 프로그래머는 캐시가 여러 개라는 사실을 의식하지 않고 공유 메모리 프로그램을 쓸 수 있다.
| 환경 | 불일치가 생기는 이유 |
|---|---|
| 멀티코어 프로세서 | 코어마다 전용 L1·L2 캐시가 있어 같은 블록이 여러 캐시에 동시에 올라간다 |
| 다중 프로세서(SMP, NUMA) | 소켓마다 캐시 계층이 따로 있다. NUMA에서는 노드 사이 인터커넥트로 일관성을 맞춘다(ccNUMA) |
| 라이트 백(Write Back) 캐시 | 수정된 값이 캐시에만 있고 주기억장치에는 아직 반영되지 않는다 |
| DMA 입출력 | 입출력 장치가 CPU 캐시를 거치지 않고 주기억장치를 직접 읽고 쓰므로, 캐시와 메모리 내용이 달라진다 |
| 이기종 가속기 공유 메모리 | GPU·가속기가 CPU와 같은 메모리를 공유한다(CXL 등) |
- 공유 데이터의 수정 : 여러 캐시에 있는 같은 블록을 한 프로세서가 수정하면 다른 캐시의 복사본이 낡는다.
- 프로세스 이주 : 프로세스가 다른 코어로 옮겨 가면, 이전 코어 캐시에 남은 수정본과 새 코어가 읽는 메모리 값이 다를 수 있다.
- 입출력 동작 : DMA가 메모리에 쓴 값을 캐시가 모르거나, 캐시에만 있는 수정본을 DMA가 모르고 메모리에서 읽어 간다.
캐시 일관성 프로토콜은 다음 두 가지를 보장한다.
- 쓰기 전파(write propagation) : 한 프로세서의 쓰기는 결국 다른 모든 프로세서에게 보인다.
- 쓰기 직렬화(write serialization) : 같은 주소에 대한 쓰기들은 모든 프로세서에게 같은 순서로 보인다.
일관성(coherence)은 같은 주소에 대한 규칙이다. 서로 다른 주소에 대한 읽기·쓰기가 어떤 순서로 보이는지는 메모리 일관성 모델(consistency model)이 정하며, 둘은 다른 개념이다.
- 공유 데이터는 캐시하지 않도록(non-cacheable) 지정한다.
- 컴파일러나 운영체제가 공유 데이터 접근 앞뒤로 캐시 플러시·무효화 명령을 넣는다.
- 하드웨어가 단순해지지만 성능 손실이 크고 판단이 보수적이어서, 오늘날에는 주로 DMA 버퍼처럼 일관성을 하드웨어가 보장하지 않는 영역에 쓴다.
하드웨어 방식은 다른 캐시의 복사본을 어떻게 처리하느냐에 따라 두 정책으로 나뉜다.
| 정책 | 동작 | 특징 |
|---|---|---|
| 쓰기 무효화(write invalidate) | 쓰기 전에 다른 캐시의 복사본을 무효화하고 혼자 쓴다 | 같은 블록에 여러 번 쓸 때 트래픽이 적다. 대부분의 상용 프로세서가 쓴다 |
| 쓰기 갱신(write update, write broadcast) | 쓸 때마다 새 값을 다른 캐시에 보내 복사본을 갱신한다 | 쓰자마자 다른 쪽이 읽는 패턴에는 유리하지만 버스 트래픽이 많다 |
그리고 복사본이 어디 있는지 아는 방법에 따라 스누피 프로토콜과 디렉토리 프로토콜로 나뉜다.
스누피(snoopy, snooping) 프로토콜은 모든 캐시 제어기가 공유 버스를 엿듣다가(snoop), 자기가 가진 블록에 대한 다른 프로세서의 읽기·쓰기 요청이 보이면 자기 복사본의 상태를 바꾸는 방식이다. 모든 요청이 버스로 방송되므로 따로 기록을 두지 않아도 누가 복사본을 가졌는지 각 캐시가 스스로 판단한다.
- 버스가 모든 요청을 방송하고 모든 캐시가 같은 순서로 본다는 점이 쓰기 직렬화를 자연스럽게 보장한다.
- 구현이 단순하고 지연이 짧다.
- 프로세서 수가 늘면 방송 트래픽이 버스 대역폭을 넘어서 확장성이 떨어진다. 그래서 수십 코어 이하의 버스 기반 SMP나 칩 내부에 쓴다.
- 대표적인 프로토콜이 MESI와 그 변형이다.
디렉토리(directory) 프로토콜은 메모리 블록마다 어느 캐시가 복사본을 가지고 있고 어떤 상태인지를 디렉토리라는 표에 기록해 두고, 일관성 메시지를 필요한 캐시에만 점대점으로 보내는 방식이다.
- 디렉토리 항목은 보통 블록 상태(미캐시·공유·독점)와 공유자 비트 벡터(프로세서마다 1비트)로 이루어진다.
- 읽기 실패가 나면 요청은 그 블록의 홈 노드(디렉토리를 가진 노드)로 가고, 홈 노드가 메모리나 소유 캐시에서 데이터를 가져다 준다.
- 쓰기 요청이 오면 홈 노드가 공유자 목록에 있는 캐시에만 무효화 메시지를 보내고 응답(ack)을 모은 뒤 쓰기를 허락한다.
- 방송이 없으므로 수백 개 이상의 프로세서로 확장할 수 있다. 대규모 ccNUMA, 메시가 깔린 많은 코어 칩에 쓴다.
- 디렉토리 저장 공간이 프로세서 수에 비례해 커지고, 홈 노드를 한 번 더 거치므로 지연이 길다. 공간을 줄이려고 제한 포인터, 희소 디렉토리, 스누프 필터 같은 기법을 쓴다.
| 구분 | 스누피 프로토콜 | 디렉토리 프로토콜 |
|---|---|---|
| 공유 상태 정보 | 각 캐시가 자기 블록 상태만 가짐(분산) | 메모리 쪽 디렉토리가 블록별 공유자 목록을 가짐(중앙 또는 노드별 분산) |
| 메시지 전달 | 버스 방송 | 필요한 캐시에만 점대점 전송 |
| 상호 연결망 | 공유 버스, 링 | 메시, 크로스바, 점대점 인터커넥트 |
| 확장성 | 낮음(수 개~수십 개) | 높음(수백 개 이상) |
| 지연 | 짧음 | 홈 노드 경유로 김 |
| 추가 하드웨어 | 스누프 로직 | 디렉토리 저장 공간과 제어기 |
| 적용 | 소규모 SMP, 칩 내부 코어 | 대규모 다중 프로세서, ccNUMA |
실제 서버 프로세서는 둘을 섞어 쓴다. 칩 안에서는 스누핑이나 스누프 필터로, 소켓 사이에서는 디렉토리로 일관성을 맞추는 식이다.
MESI는 쓰기 무효화 방식의 대표적인 스누피 프로토콜로, 캐시 블록(캐시 라인)마다 네 가지 상태 중 하나를 둔다. 상태 이름의 머리글자를 따서 MESI라 부르며, 1984년 일리노이 대학에서 발표되어 일리노이 프로토콜이라고도 한다. 인텔 x86 프로세서가 펜티엄 이후 이 계열을 쓴다.
| 상태 | 의미 | 메모리와 같은가 | 다른 캐시에 복사본 |
|---|---|---|---|
| Modified(수정) | 이 캐시만 가진 수정된 블록 | 다름(dirty) | 없음 |
| Exclusive(독점) | 이 캐시만 가진 수정되지 않은 블록 | 같음(clean) | 없음 |
| Shared(공유) | 여러 캐시가 함께 가진 수정되지 않은 블록 | 같음 | 있을 수 있음 |
| Invalid(무효) | 유효한 데이터가 아님 | - | - |
자기 프로세서의 요청(로컬)에 따른 전이:
- 읽기 실패 : 버스에 읽기 요청을 낸다. 다른 캐시가 그 블록을 가지고 있으면 S, 아무도 없으면 E로 적재한다.
- 읽기 적중 : 상태가 바뀌지 않는다.
- 쓰기 적중(S 상태) : 버스에 무효화 요청을 내 다른 복사본을 I로 만든 뒤 M이 된다.
- 쓰기 적중(E 상태) : 다른 복사본이 없으므로 버스 트래픽 없이 바로 M이 된다. E 상태를 따로 둔 이유가 이것이다.
- 쓰기 적중(M 상태) : 그대로 M이다.
- 쓰기 실패 : 소유권을 요구하는 읽기(RFO, Read For Ownership)를 내 블록을 가져오면서 다른 복사본을 무효화하고 M이 된다.
다른 프로세서의 요청을 스누핑해서 일어나는 전이:
- M 상태에서 남의 읽기를 봄 : 수정된 블록을 메모리(또는 요청한 캐시)에 내려 주고 S가 된다.
- M 상태에서 남의 쓰기(RFO)를 봄 : 블록을 내려 주고 I가 된다.
- E 상태에서 남의 읽기를 봄 : S가 된다.
- E·S 상태에서 남의 쓰기·무효화를 봄 : I가 된다.
- 코어 0이 변수 x를 읽는다. 아무도 x가 없으므로 코어 0의 x는 E.
- 코어 1이 x를 읽는다. 코어 0이 스누핑하고 S로 바뀌며, 코어 1의 x도 S.
- 코어 0이 x에 쓴다. 무효화 요청이 나가 코어 1의 x는 I, 코어 0의 x는 M.
- 코어 1이 x를 다시 읽는다. 코어 0이 수정된 값을 내려 주고 둘 다 S가 된다. 코어 1은 낡은 값이 아니라 새 값을 읽는다.
| 프로토콜 | 추가 상태 | 목적 | 사용 예 |
|---|---|---|---|
| MSI | (E 없음) | 가장 단순한 형태 | 교재·초기 시스템 |
| MOESI | Owned | 수정된 블록을 메모리에 쓰지 않고 다른 캐시와 공유. O를 가진 캐시가 데이터를 공급하고 나중에 메모리에 쓴다 | AMD |
| MESIF | Forward | 여러 S 복사본 중 F 상태 하나만 요청에 응답해 중복 응답을 막는다 | 인텔(QPI 이후) |
- 거짓 공유(false sharing) : 서로 다른 변수가 같은 캐시 라인에 들어 있으면, 각 코어가 자기 변수만 써도 라인 전체가 코어 사이를 오가며 무효화가 반복된다. 자주 쓰는 변수를 캐시 라인 크기(보통 64바이트)에 맞춰 떨어뜨려 둔다.
- 일관성 실패(coherence miss) : 다른 코어의 쓰기로 무효화되어 생기는 캐시 실패. 기존의 3C(강제·용량·충돌) 실패에 더해 네 번째 C로 부른다.
- 락 경합 : 여러 코어가 한 락 변수를 계속 쓰면 라인이 오가며 성능이 떨어진다. 락을 얻기 전에 읽기만 반복하는 방식(TTAS)으로 무효화 트래픽을 줄인다.