Memory Hierarchy: 속도와 용량의 트레이드오프를 계층으로 해결하기
메모리 계층구조는 레지스터부터 디스크까지 접근 속도와 용량이 다른 저장장치를 계층적으로 배치해 성능과 비용의 균형을 맞추는 컴퓨터 구조 원리다. 프로그램이 최근 사용한 데이터와 그 주변 데이터를 다시 사용할 확률이 높다는 지역성(locality) 원리를 이용해 상위 계층(캐시)에 자주 쓰는 데이터를 올려두는 방식으로 동작한다. 이 원리를 이해하면 왜 배열
1. 개념
메모리 계층구조(memory hierarchy)는 CPU 레지스터, 캐시(L1/L2/L3), 주기억장치(RAM), 보조기억장치(SSD/HDD)를 접근 속도가 빠르고 용량이 작은 순서대로 피라미드 형태로 배치한 구조다.
계층을 위로 올라갈수록 접근 속도는 빨라지지만 용량은 작아지고 비트당 비용은 비싸진다. 반대로 아래로 내려갈수록 느리지만 용량이 크고 저렴하다.
레지스터 (수백 바이트, 서브 나노초)
↑↓
L1 캐시 (수십 KB, ~1ns)
↑↓
L2 캐시 (수백 KB~수 MB, ~4~10ns)
↑↓
L3 캐시 (수 MB~수십 MB, ~10~20ns)
↑↓
주기억장치(RAM) (수 GB~수백 GB, ~50~100ns)
↑↓
SSD (수백 GB~수 TB, ~10~100μs)
↑↓
HDD (수 TB, ~1~10ms)2. 왜 사용하는가
이상적으로는 모든 메모리가 레지스터만큼 빠르면 좋겠지만, 빠른 메모리(SRAM)는 회로가 복잡하고 전력 소모가 커서 대용량으로 만들기엔 비용이 지나치게 높다. 느린 메모리(DRAM, 플래시)는 대용량화가 쉽고 저렴하지만 접근 속도가 느리다.
계층구조는 "자주 쓰는 데이터는 빠른 곳에, 가끔 쓰는 데이터는 느린 곳에" 두는 전략으로 이 딜레마를 해결한다. 이게 가능한 이유는 실제 프로그램의 메모리 접근 패턴이 무작위가 아니라 지역성(locality)을 갖기 때문이다.
3. 동작 원리
지역성 원리는 두 가지로 나뉜다.
- 시간 지역성(temporal locality): 최근에 접근한 데이터는 곧 다시 접근될 가능성이 높다. 예: 반복문 안의 변수.
- 공간 지역성(spatial locality): 어떤 주소에 접근하면 그 주변 주소도 곧 접근될 가능성이 높다. 예: 배열의 연속된 원소.
캐시는 이 원리를 이용해 데이터를 "캐시 라인(cache line)" 단위(보통 64바이트)로 한꺼번에 가져온다. CPU가 특정 메모리 주소를 요청하면:
- 캐시에 데이터가 있으면 캐시 히트(cache hit) — 빠르게 반환.
- 캐시에 없으면 캐시 미스(cache miss) — 하위 계층(RAM)에서 캐시 라인 단위로 데이터를 가져와 캐시에 적재한 뒤 반환.
캐시가 가득 차면 LRU(least recently used) 등의 교체 정책으로 오래된 데이터를 밀어낸다. 이 과정이 하드웨어(캐시 컨트롤러)에서 자동으로 일어나기 때문에 프로그래머가 직접 캐시를 제어할 수는 없지만, 데이터 접근 패턴을 설계해서 캐시 히트율을 높일 수는 있다.
4. 코드 예제
배열을 행 우선(row-major) 순서로 순회할 때와 열 우선으로 순회할 때 공간 지역성 차이를 확인하는 예제다. Python은 인터프리터 오버헤드 때문에 차이가 크게 드러나지 않을 수 있지만, NumPy 배열(연속 메모리)로 실험하면 캐시 효과를 관찰할 수 있다.
import numpy as np
import time
n = 4000
matrix = np.random.rand(n, n)
# 행 우선 순회: 메모리 상 연속된 주소를 순서대로 접근 (공간 지역성 좋음)
start = time.perf_counter()
total = 0.0
for i in range(n):
for j in range(n):
total += matrix[i, j]
row_major_time = time.perf_counter() - start
# 열 우선 순회: 매번 다음 캐시 라인으로 건너뜀 (공간 지역성 나쁨)
start = time.perf_counter()
total = 0.0
for j in range(n):
for i in range(n):
total += matrix[i, j]
col_major_time = time.perf_counter() - start
print(f"행 우선 순회: {row_major_time:.3f}초")
print(f"열 우선 순회: {col_major_time:.3f}초")NumPy 배열은 기본적으로 C 스타일(행 우선, row-major)로 메모리에 저장되므로, 행 우선 순회가 인접한 메모리 주소를 순서대로 읽어 캐시 히트율이 높다. 열 우선 순회는 매 접근마다 큰 보폭(stride)으로 메모리를 건너뛰어 캐시 미스가 자주 발생한다.
5. 시간 복잡도 또는 성능 특성
계층별 대략적인 접근 지연(latency)은 다음과 같다 (2020년대 일반적인 x86 서버 기준 근사치이며 하드웨어마다 다르다).
| 계층 | 접근 지연 | 상대 배수 (L1 대비) | |---|---|---| | L1 캐시 | ~1ns | 1배 | | L2 캐시 | ~4~10ns | 4~10배 | | L3 캐시 | ~10~20ns | 10~20배 | | RAM | ~50~100ns | 50~100배 | | SSD (랜덤 읽기) | ~10~100μs | 10,000~100,000배 | | HDD (랜덤 시크) | ~1~10ms | 1,000,000배 이상 |
RAM 접근이 L1 캐시보다 100배 가까이 느리고, SSD는 RAM보다 다시 100~1000배 느리다는 점이 핵심이다. 이 격차 때문에 캐시 미스율을 1%p 줄이는 것만으로도 전체 프로그램 성능이 크게 달라질 수 있다.
6. 실무 사용 사례
- DB 버퍼 풀(buffer pool): PostgreSQL, MySQL 등은 디스크 I/O를 줄이기 위해 자주 조회되는 페이지를 RAM에 캐싱한다.
shared_buffers설정이 이에 해당한다. - 애플리케이션 캐시: Redis, Memcached는 RAM 계층을 활용해 DB(디스크 계층) 접근을 줄인다.
- CDN(content delivery network): 지리적으로 가까운 엣지 서버에 정적 자원을 캐싱해 "네트워크 지역성"을 활용한다.
- 알고리즘 설계: 캐시 친화적(cache-friendly) 자료구조 설계 시 배열 기반 구조(예: B-tree, 배열 힙)가 포인터 기반 구조(연결 리스트, 이진 탐색 트리)보다 실제 성능이 좋은 경우가 많다. 이는 빅오 표기법상 복잡도가 같아도 캐시 미스 횟수가 다르기 때문이다.
-- PostgreSQL에서 버퍼 캐시 히트율 확인 예시
SELECT
relname,
heap_blks_read AS disk_reads,
heap_blks_hit AS cache_hits,
round(heap_blks_hit::numeric / nullif(heap_blks_hit + heap_blks_read, 0) * 100, 2) AS cache_hit_ratio
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC
LIMIT 10;7. 주의할 점
- 캐시 히트율은 빅오 표기법에 나타나지 않는 성능 요소다. 알고리즘의 시간복잡도가 같아도 메모리 접근 패턴에 따라 실제 실행 시간은 몇 배씩 차이 날 수 있다.
- 멀티코어 환경에서는 캐시 일관성(cache coherence) 문제가 발생한다. 여러 코어가 같은 캐시 라인을 수정하면 캐시 라인 전체가 무효화되는 "거짓 공유(false sharing)" 현상이 생겨 오히려 성능이 떨어질 수 있다.
- 애플리케이션 레벨 캐시(Redis 등)를 추가한다고 항상 빨라지는 건 아니다. 캐시 자체의 네트워크 왕복 시간(RTT)이 DB 쿼리보다 느릴 수도 있으므로 실측이 필요하다.
- JIT 컴파일러나 가비지 컬렉션이 있는 언어(JavaScript, Java, Python)에서는 객체가 힙 여기저기 흩어져 있어 배열만큼 확실한 공간 지역성을 보장하기 어렵다.
8. 핵심 정리
메모리 계층구조는 "빠르지만 비싸고 작은 메모리"와 "느리지만 싸고 큰 메모리"를 조합해 평균적으로 빠르면서도 대용량인 시스템을 만드는 구조다. 이는 시간 지역성과 공간 지역성이라는 실제 프로그램의 접근 패턴 특성을 전제로 성립한다. 개발자가 캐시를 직접 제어할 수는 없지만, 연속된 메모리를 순차적으로 접근하는 자료구조와 알고리즘을 설계하면 캐시 히트율을 높여 실질적인 성능 향상을 얻을 수 있다.