프로그램이 느려질 때 병목 지점은 CPU가 아니라 메모리일 수 있다. 특히 CPU는 매우 빠르게 계산하지만 필요한 데이터를 메모리에서 가져오는 속도가 느리면 오히려 기다리는 시간이 길어진다. 이런 상황에서 등장하는 개념이 바로 캐시(Cache)와 메인 메모리(Main Memory)이다. 오늘은 이 두 메모리 구조의 역할과 차이점, 동작 원리에 대해 정리해본다.
현대 컴퓨터는 속도와 비용의 균형을 맞추기 위해 계층적 메모리 구조(Hierarchy)를 갖는다. 아래는 대표적인 구조이다.
CPU 레지스터 < 캐시 메모리 < 메인 메모리(RAM) < 보조 저장장치(HDD/SSD)
빠름 ↑ 속도 ↓ 느림
작음 ↓ 용량 ↑ 큼
비쌈 ↓ 비용 ↑ 쌈
메인 메모리는 우리가 흔히 말하는 RAM(Random Access Memory)이다. CPU가 직접 데이터를 처리하려면 해당 데이터를 메인 메모리에서 가져와야 한다. 이때 프로그램 실행에 필요한 코드, 변수, 힙 영역 등이 모두 메인 메모리에 올라간다.
RAM은 휘발성 메모리로 컴퓨터 전원이 꺼지면 저장된 데이터도 모두 사라진다. 프로그램이 실행되면 실행 파일이 하드디스크에서 메인 메모리로 로드되고 CPU는 메모리 주소를 통해 해당 데이터를 읽고 쓴다.
메인 메모리의 주요 특징은 다음과 같다.
결국 메인 메모리는 프로그램의 작업 공간이라고 생각하면 된다. CPU가 실제 작업을 하려면 이 메모리로부터 데이터를 읽고 연산 결과를 다시 여기에 기록한다. 하지만 CPU는 매우 빠르게 동작하기 때문에 이 메모리 접근 속도가 느리면 성능 저하가 발생할 수 있다.
캐시 메모리는 CPU와 메인 메모리 사이에서 속도 차이를 줄이기 위한 완충 장치 역할을 한다. 메인 메모리는 비교적 느리기 때문에, CPU는 자주 사용하는 데이터를 빠르게 접근하기 위해 자신만의 고속 저장소를 별도로 둔다. 이게 바로 캐시 메모리이다.
캐시는 CPU 내부 혹은 가까이에 있으며 접근 속도가 메모리보다 훨씬 빠르다. 하지만 대신 용량은 매우 작다. 이러한 캐시는 보통 3단계 계층 구조로 존재한다.
캐시는 일반적으로 SRAM(Static RAM)을 사용하며 DRAM보다 훨씬 빠르지만 비용이 높고 집적도가 낮아 용량을 크게 늘릴 수 없다.
캐시 메모리는 메인 메모리보다 수십 배 빠르므로 CPU는 가능한 한 캐시에서 모든 데이터를 처리하려고 한다. 만약 캐시에 원하는 데이터가 없을 경우 그제야 메인 메모리로 요청을 보내고 가져온 데이터를 다시 캐시에 저장한다.
CPU의 클럭 속도는 수 GHz 수준으로 1초에 수십억 번의 연산이 가능하다. 반면 메인 메모리는 수십~수백 ns 단위로 반응하기 때문에 CPU가 메모리 접근을 기다려야 하는 시간이 CPU 연산 시간보다 훨씬 길다. 이럴 경우 CPU는 계산보다 기다리는 데 더 많은 시간을 쓰게 된다. 이것을 Memory Bottleneck이라고 한다.
이러한 병목 현상을 해결하기 위해 캐시 메모리가 도입되었으며 그 기반에는 지역성(Locality)이라는 개념이 있다.
예를 들어 반복문에서 배열의 요소들을 순서대로 접근하는 경우 arr[0]
, arr[1]
, arr[2]
… 는 공간 지역성을 만족한다. 이때 CPU는 한 번에 인접한 여러 데이터를 캐시에 불러와 저장함으로써 반복적으로 메모리에 접근하지 않게 최적화할 수 있다.
예를 들어 아래와 같은 코드가 있다고 가정하자.
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += arr[i];
}
이 코드는 배열의 요소들을 순서대로 읽으며 합산하는 구조다. CPU가 arr[0]
을 읽을 때 캐시는 이 주소뿐만 아니라 그 근처 메모리 블록도 함께 불러온다. 예를 들어 arr[0]
~ arr[31]
까지 미리 캐시에 저장해둔다.
이후 반복문이 arr[1]
, arr[2]
를 읽을 때 이미 캐시에 있기 때문에 CPU는 RAM에 다시 접근하지 않아도 된다. 이로 인해 프로그램의 실행 속도는 훨씬 빨라진다.
이러한 캐시의 데이터 저장 단위를 캐시 라인(Cache Line)이라고 하며, 일반적으로 64바이트 단위로 작동한다.
캐시 메모리에 CPU가 원하는 데이터가 존재하지 않을 때, 이를 캐시 미스(Cache Miss)라고 한다. 이 경우 CPU는 메인 메모리로부터 데이터를 불러와야 하므로 더 많은 시간이 걸린다.
캐시 미스는 다음과 같이 세 가지로 분류된다.
Cold Miss (Compulsory Miss)
해당 데이터가 처음 요청되어 아직 캐시에 없는 경우 발생
Capacity Miss
캐시 용량이 작아 전체 데이터를 모두 담을 수 없을 때 발생. 이전에 있던 데이터가 캐시에서 밀려나 다른 데이터를 넣었는데 다시 해당 데이터가 필요해졌을 경우
Conflict Miss
서로 다른 주소의 데이터가 같은 캐시 라인에 매핑되는 경우 발생. 즉, 같은 위치에 덮어쓰는 충돌로 인해 발생
캐시 미스를 줄이기 위해 사전 로딩(Prefetching), 교체 정책(예: LRU), 연관도 증가(Set-associativity) 등의 최적화 기법이 사용된다.
항목 | 메인 메모리(RAM) | 캐시 메모리(Cache) |
---|---|---|
위치 | CPU 외부 | CPU 내부 또는 근처 |
속도 | 느림 (수십 ns) | 매우 빠름 (1~10 ns) |
용량 | 큼 (GB 수준) | 작음 (KB~MB 수준) |
기술 | DRAM | SRAM |
비용 | 상대적으로 저렴 | 비쌈 |
사용 목적 | 실행 중인 전체 데이터 | 자주 사용하는 소량의 데이터 |
처음에는 단순히 RAM과 캐시의 속도 차이만 존재하는 줄 알았지만 실제로는 CPU와 메모리 간 병목을 줄이기 위한 캐시 계층 구조, 그리고 그 안에 존재하는 시간/공간 지역성 기반의 최적화 원리가 굉장히 흥미로웠다. 특히 캐시 메모리가 왜 필요한지를 이해하기 위해 메모리 접근 시간의 차이를 수치로 비교해보면서 CPU의 빠른 연산 성능이 오히려 메모리 지연에 발목 잡힐 수 있다는 사실이 인상 깊었다. 또한 캐시 미스(Cache Miss)
가 어떻게 발생하고 이를 줄이기 위해 어떤 기법들이 사용되는지도 함께 익힐 수 있었다. 앞으로 성능 이슈를 분석하거나 시스템 구조를 설계할 때 단순히 CPU 속도만 고려할 게 아니라 메모리 계층 전반에 걸친 데이터 흐름과 데이터 접근 패턴도 함께 고려해야겠다는 생각이 들었다.
참고