이성열 Seongyeol Yi

빠른 계산 느린 기억

25.07

지난 글에서 Fetch-Decode-Execute 사이클을 통해 CPU가 명령어를 처리하는 과정을 살펴보았습니다. 시뮬레이터에서는 메모리 접근이 즉시 이루어졌지만, 실제로는 그렇지 않습니다. CPU와 메모리 사이에는 수백 배의 속도 차이가 있어 CPU가 아무리 빨라도 메모리를 기다리는 시간이 병목이 됩니다. 메모리를 빠르게 만들면 용량이 작아지고 용량을 키우면 느려집니다. 빠르면서 큰 메모리는 만들 수 없습니다.

이 문제를 어떻게 해결할 수 있을까요?

책상, 서랍, 창고

일상에서 비슷한 문제를 이미 해결하고 있습니다. 지금 공부하고 있다고 생각해 봅시다. 자주 보는 교과서는 책상 위에 펼쳐 두고, 가끔 참고하는 자료집은 서랍에 넣어 두고, 이번 학기에 안 쓰는 책은 창고에 보관합니다.

왜 이렇게 할까요? 모든 책을 창고에 두면 꺼내러 갈 때마다 시간이 낭비됩니다. 반대로 모든 책을 책상 위에 올려놓을 수는 없습니다. 책상은 공간이 좁기 때문입니다. 그래서 자주 쓰는 것일수록 가까이 두는 전략을 자연스럽게 사용합니다.

컴퓨터에서도 같은 전략을 쓸 수 있지 않을까요? 그런데 “자주 쓰는 데이터”가 정말 있긴 한 걸까요?

지역성

프로그램이 데이터에 접근하는 패턴을 관찰하면 흥미로운 규칙을 발견할 수 있습니다.

시간적 지역성(temporal locality): 한번 접근한 데이터는 곧 다시 접근할 가능성이 높습니다. 반복문에서 같은 변수를 매번 읽고 쓰는 것이 대표적입니다.

메모리 주소
0
1
2
3
4
5
6
7

카운트다운 프로그램처럼 같은 명령어를 반복 실행하면 주소 0~3이 계속 다시 접근됩니다.

공간적 지역성(spatial locality): 접근한 데이터 근처의 데이터도 곧 접근할 가능성이 높습니다. 연속된 데이터를 처음부터 끝까지 순서대로 읽는 경우가 이에 해당합니다.

메모리 주소
0
1
2
3
4
5
6
7

연속된 데이터를 읽을 때 인접한 주소가 차례로 접근됩니다.

프로그램은 전체 메모리를 골고루 쓰지 않습니다. 특정 영역에 집중적으로 접근하는 경향이 있습니다. 그렇다면 자주 쓰는 데이터를 CPU 가까이에 두는 작고 빠른 저장소를 만들면 어떨까요?

캐시

그 저장소가 바로 캐시(cache)입니다. 캐시는 CPU와 RAM 사이에 위치한 소용량 고속 메모리입니다. CPU가 데이터를 요청하면 먼저 캐시를 확인합니다.

메모리 주소 — 눌러서 접근
캐시
····
RAM
모든 주소 보유

다음 순서대로 조작해봅시다:

  1. 메모리 주소를 하나 클릭합니다. 처음에는 캐시가 비어 있으니 미스가 발생합니다.
  2. 같은 주소를 다시 클릭합니다. 이번에는 캐시에 있으므로 히트가 발생합니다.
  3. 서로 다른 주소를 5개 이상 클릭합니다. 캐시가 4칸뿐이라 가장 오래전에 접근한 데이터가 밀려납니다.

캐시가 가득 차면 가장 오래전에 사용된 데이터를 내보내고 새 데이터를 넣습니다. 전체 접근 중 히트의 비율을 적중률(hit rate)이라 합니다. 적중률이 높을수록 CPU가 RAM을 기다리는 시간이 줄어듭니다.

그렇다면 지역성이 있는 패턴과 없는 패턴은 적중률이 얼마나 다를까요?

지역성과 적중률

캐시 크기가 4인 상황에서 두 패턴을 비교해 봅시다.

·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
히트미스
적중률 -%
  1. “순차 접근” 버튼을 누릅니다. 0-3을 두 번 반복하고 4-7을 두 번 반복하는 패턴입니다. 두 번째 반복부터 히트가 발생하며 적중률이 높습니다.
  2. “랜덤 접근” 버튼을 누릅니다. 같은 횟수지만 무작위 주소에 접근합니다. 적중률이 크게 떨어집니다. 여러 번 눌러서 비교해 봅시다.

캐시가 효과적인 이유는 지역성 덕분입니다. 프로그램이 데이터에 접근하는 패턴에 지역성이 있기 때문에, 작은 캐시만으로도 높은 적중률을 달성할 수 있습니다. 랜덤 접근처럼 지역성이 없으면 캐시의 효과는 사라집니다.

그런데 캐시를 크게 만들면 속도가 느려집니다. 크기와 속도를 동시에 만족시킬 수 없으니, 단계를 나누면 어떨까요?

메모리 계층 구조

캐시 하나를 크게 만드는 대신, 작고 빠른 캐시와 크고 느린 캐시를 여러 단계로 쌓으면 어떨까요? 지역성 덕분에 대부분의 접근은 가장 작고 빠른 첫 번째 단계에서 끝납니다. 가끔 거기서 못 찾을 때만 다음 단계로 내려가면 됩니다. 이렇게 하면 하나의 큰 캐시보다 평균 접근 속도가 더 빨라집니다.

실제 컴퓨터가 이 방식을 씁니다. CPU에 가까운 쪽은 작고 빠르게, 먼 쪽은 크고 느리게 만들어 단계별로 연결합니다. 이를 메모리 계층 구조(memory hierarchy)라 합니다.

계층용량속도
레지스터수십 개1 사이클
L1 캐시수십 KB~4 사이클
L2 캐시수백 KB~10 사이클
L3 캐시수십 MB~40 사이클
RAM수 GB ~ 수십 GB~200 사이클

위로 갈수록 빠르고 작으며, 아래로 갈수록 느리고 큽니다. 각 계층은 바로 아래 계층의 캐시 역할을 합니다. CPU가 데이터를 요청하면 L1부터 확인하고, 없으면 L2, 그래도 없으면 L3, 최종적으로 RAM까지 내려갑니다.

메모리 주소 — 눌러서 접근
L1
··
~4 사이클
L2
····
~10 사이클
L3
········
~40 사이클
RAM
모든 주소 보유
~200 사이클
  1. 같은 주소를 반복해서 클릭합니다. L1에서 바로 히트가 납니다.
  2. 서로 다른 주소를 많이 클릭합니다. 상위 캐시에서 밀려나 더 아래 계층까지 내려가야 합니다.

위 시뮬레이터에서는 L1이 2칸뿐이라 금방 밀려나지만, 실제 L1 캐시는 수십 KB로 대부분의 접근이 여기서 끝납니다. 덕분에 CPU는 마치 크고 빠른 메모리를 쓰는 것처럼 동작할 수 있습니다.

RAM 아래에는 SSD나 HDD 같은 보조기억장치가 있습니다. 여기까지 내려가면 접근 시간이 수만~수십만 사이클로 늘어납니다.

마무리

이 시리즈의 첫 글에서 0과 1로 시작해, 논리 게이트를 배우고, MUX로 선택을 하고, 가산기로 계산을 하고, 래치로 기억을 하고, 튜링 머신폰 노이만 구조로 컴퓨터의 설계 원리를 세우고, CPU로 명령어를 실행하고, 캐시와 메모리 계층 구조로 속도 문제까지 해결했습니다. 0과 1에서 출발해 동작하는 컴퓨터 한 대를 조립한 셈입니다.

컴퓨터의 구조를 알았으니, 이제 데이터를 효율적으로 다루는 방법이 필요합니다. 같은 문제라도 데이터를 어떻게 정리하고 어떤 순서로 처리하느냐에 따라 성능이 달라집니다. 다음 파트에서는 자료구조와 알고리즘을 다룹니다.