지난 글에서 Fetch-Decode-Execute 사이클을 통해 CPU가 명령어를 처리하는 과정을 살펴보았다. 시뮬레이터에서는 메모리 접근이 즉시 이루어졌지만, 실제로는 그렇지 않다. CPU와 메모리 사이에는 수백 배의 속도 차이가 있어 CPU가 아무리 빨라도 메모리를 기다리는 시간이 병목이 된다. 메모리를 빠르게 만들면 용량이 작아지고 용량을 키우면 느려진다. 빠르면서 큰 메모리는 만들 수 없다.
이 문제를 어떻게 해결할 수 있을까?
일상에서 비슷한 문제를 이미 해결하고 있다. 지금 공부하고 있다고 생각해 보자. 자주 보는 교과서는 책상 위에 펼쳐 두고, 가끔 참고하는 자료집은 서랍에 넣어 두고, 이번 학기에 안 쓰는 책은 창고에 보관한다.
왜 이렇게 할까? 모든 책을 창고에 두면 꺼내러 갈 때마다 시간이 낭비된다. 반대로 모든 책을 책상 위에 올려놓을 수는 없다. 책상은 공간이 좁기 때문이다. 그래서 자주 쓰는 것일수록 가까이 두는 전략을 자연스럽게 사용한다.
컴퓨터에서도 같은 전략을 쓸 수 있지 않을까? 그런데 “자주 쓰는 데이터”가 정말 있긴 한 걸까?
프로그램이 데이터에 접근하는 패턴을 관찰하면 흥미로운 규칙을 발견할 수 있다.
시간적 지역성(temporal locality): 한번 접근한 데이터는 곧 다시 접근할 가능성이 높다. 반복문에서 같은 변수를 매번 읽고 쓰는 것이 대표적이다.
카운트다운 프로그램처럼 같은 명령어를 반복 실행하면 주소 0~3이 계속 다시 접근된다.
공간적 지역성(spatial locality): 접근한 데이터 근처의 데이터도 곧 접근할 가능성이 높다. 연속된 데이터를 처음부터 끝까지 순서대로 읽는 경우가 이에 해당한다.
연속된 데이터를 읽을 때 인접한 주소가 차례로 접근된다.
프로그램은 전체 메모리를 골고루 쓰지 않는다. 특정 영역에 집중적으로 접근하는 경향이 있다. 그렇다면 자주 쓰는 데이터를 CPU 가까이에 두는 작고 빠른 저장소를 만들면 어떨까?
그 저장소가 바로 캐시(cache)다. 캐시는 CPU와 RAM 사이에 위치한 소용량 고속 메모리다. CPU가 데이터를 요청하면 먼저 캐시를 확인한다.
다음 순서대로 조작해보자:
캐시가 가득 차면 가장 오래전에 사용된 데이터를 내보내고 새 데이터를 넣는다. 전체 접근 중 히트의 비율을 적중률(hit rate)이라 한다. 적중률이 높을수록 CPU가 RAM을 기다리는 시간이 줄어든다.
그렇다면 지역성이 있는 패턴과 없는 패턴은 적중률이 얼마나 다를까?
캐시 크기가 4인 상황에서 두 패턴을 비교해 보자.
캐시가 효과적인 이유는 지역성 덕분이다. 프로그램이 데이터에 접근하는 패턴에 지역성이 있기 때문에, 작은 캐시만으로도 높은 적중률을 달성할 수 있다. 랜덤 접근처럼 지역성이 없으면 캐시의 효과는 사라진다.
그런데 캐시를 크게 만들면 속도가 느려진다. 크기와 속도를 동시에 만족시킬 수 없으니, 단계를 나누면 어떨까?
캐시 하나를 크게 만드는 대신, 작고 빠른 캐시와 크고 느린 캐시를 여러 단계로 쌓으면 어떨까? 지역성 덕분에 대부분의 접근은 가장 작고 빠른 첫 번째 단계에서 끝난다. 가끔 거기서 못 찾을 때만 다음 단계로 내려가면 된다. 이렇게 하면 하나의 큰 캐시보다 평균 접근 속도가 더 빨라진다.
실제 컴퓨터가 이 방식을 쓴다. CPU에 가까운 쪽은 작고 빠르게, 먼 쪽은 크고 느리게 만들어 단계별로 연결한다. 이를 메모리 계층 구조(memory hierarchy)라 한다.
| 계층 | 용량 | 속도 |
|---|---|---|
| 레지스터 | 수십 개 | 1 사이클 |
| L1 캐시 | 수십 KB | ~4 사이클 |
| L2 캐시 | 수백 KB | ~10 사이클 |
| L3 캐시 | 수십 MB | ~40 사이클 |
| RAM | 수 GB ~ 수십 GB | ~200 사이클 |
위로 갈수록 빠르고 작으며, 아래로 갈수록 느리고 크다. 각 계층은 바로 아래 계층의 캐시 역할을 한다. CPU가 데이터를 요청하면 L1부터 확인하고, 없으면 L2, 그래도 없으면 L3, 최종적으로 RAM까지 내려간다.
위 시뮬레이터에서는 L1이 2칸뿐이라 금방 밀려나지만, 실제 L1 캐시는 수십 KB로 대부분의 접근이 여기서 끝난다. 덕분에 CPU는 마치 크고 빠른 메모리를 쓰는 것처럼 동작할 수 있다.
RAM 아래에는 SSD나 HDD 같은 보조기억장치가 있다. 여기까지 내려가면 접근 시간이 수만~수십만 사이클로 늘어난다.
이 시리즈의 첫 글에서 0과 1로 시작해, 논리 게이트를 배우고, MUX로 선택을 하고, 가산기로 계산을 하고, 래치로 기억을 하고, 튜링 머신과 폰 노이만 구조로 컴퓨터의 설계 원리를 세우고, CPU로 명령어를 실행하고, 캐시와 메모리 계층 구조로 속도 문제까지 해결했다. 0과 1에서 출발해 동작하는 컴퓨터 한 대를 조립한 셈이다.
컴퓨터의 구조를 알았으니, 이제 데이터를 효율적으로 다루는 방법이 필요하다. 같은 문제라도 데이터를 어떻게 정리하고 어떤 순서로 처리하느냐에 따라 성능이 달라진다. 다음 파트에서는 자료구조와 알고리즘을 다룬다.