[Daily morning study] 캐시 메모리 계층 구조 (L1/L2/L3)

#daily morning study

Image


왜 캐시 메모리가 필요한가

CPU와 메인 메모리(RAM) 사이에는 속도 차이가 수백 배 이상 존재한다. CPU가 연산을 마친 뒤 RAM에서 다음 데이터를 가져올 때까지 기다리는 시간을 메모리 레이턴시라고 하는데, 이 병목이 전체 성능을 크게 제한한다.

이 격차를 줄이기 위해 CPU와 RAM 사이에 작고 빠른 메모리를 여러 단계로 배치한 것이 캐시 메모리 계층 구조(Cache Memory Hierarchy)다.


메모리 계층 구조 전체 그림

CPU 레지스터   → 가장 빠름, 수 바이트 ~ 수십 바이트
L1 캐시        → ~수십 KB, ~1~4 사이클
L2 캐시        → ~수백 KB, ~10~20 사이클
L3 캐시        → ~수 MB ~ 수십 MB, ~30~50 사이클
RAM (DRAM)     → ~수십 GB, ~100~200 사이클
SSD / HDD      → 수백 GB ~ 수 TB, 수백만 사이클

각 단계는 위로 갈수록 빠르고 작으며, 아래로 갈수록 크고 느리다.


L1 / L2 / L3 캐시 비교

항목L1 캐시L2 캐시L3 캐시
위치코어 내부코어 내부 or 근접여러 코어가 공유
크기32KB ~ 64KB256KB ~ 1MB4MB ~ 64MB
레이턴시1~4 사이클10~20 사이클30~50 사이클
역할가장 자주 쓰는 데이터L1 miss 시 검색멀티코어 공유 데이터
  • L1은 보통 명령어 캐시(I-cache)데이터 캐시(D-cache)로 분리되어 있다.
  • L2는 L1 미스 시 먼저 탐색되는 “중간 버퍼” 역할을 한다.
  • L3는 여러 코어가 공유하기 때문에 코어 간 데이터 공유와 일관성 유지에도 중요하다.

캐시 히트(Hit)와 미스(Miss)

CPU가 데이터를 요청할 때:

  1. L1 캐시 확인 → 있으면 캐시 히트(L1 Hit), 없으면 L1 미스
  2. L2 캐시 확인 → 있으면 L2 Hit, 없으면 L2 미스
  3. L3 캐시 확인 → 있으면 L3 Hit, 없으면 L3 미스
  4. RAM 접근 → 느리지만 데이터를 가져온 뒤 캐시에 적재

캐시 미스가 많아질수록 CPU가 데이터를 기다리는 스톨(stall) 상태가 늘어나 성능이 저하된다.


지역성(Locality) 원리

캐시가 효과적으로 동작하는 이유는 프로그램이 지역성이라는 패턴을 따르기 때문이다.

시간적 지역성 (Temporal Locality)

한 번 접근한 데이터는 가까운 미래에 다시 접근될 가능성이 높다.

// i 변수가 루프 내에서 반복적으로 사용 → 시간적 지역성
for (int i = 0; i < n; i++) {
    sum += arr[i];
}

공간적 지역성 (Spatial Locality)

접근한 데이터 주변 주소의 데이터도 곧 접근될 가능성이 높다.

// arr[0], arr[1], arr[2]... 순서대로 접근 → 공간적 지역성
for (int i = 0; i < n; i++) {
    sum += arr[i];
}

캐시는 이 원리를 이용해 데이터를 캐시 라인(Cache Line) 단위(보통 64바이트)로 통째로 가져온다. 한 요소를 가져올 때 인접 요소도 함께 캐시에 올리는 것이다.


캐시 교체 정책 (Eviction Policy)

캐시가 가득 찼을 때 어느 데이터를 버릴지 결정하는 정책이다.

정책설명
LRU (Least Recently Used)가장 오래 전에 사용된 데이터 제거 (가장 많이 쓰임)
LFU (Least Frequently Used)사용 빈도가 가장 낮은 데이터 제거
FIFO가장 먼저 들어온 데이터 제거
Random무작위 제거 (구현 단순)

실제 CPU 캐시는 완전한 LRU보다 구현이 단순한 Pseudo-LRU 등의 변형을 쓴다.


쓰기 정책 (Write Policy)

CPU가 캐시에 데이터를 쓸 때 RAM과 어떻게 동기화할지 결정하는 방식이다.

Write-Through

  • 캐시에 쓰는 동시에 RAM에도 즉시 반영한다.
  • 데이터 일관성이 보장되지만 쓰기마다 RAM 접근이 발생해 느리다.

Write-Back

  • 캐시에만 쓰고, 해당 캐시 라인이 교체될 때 RAM에 반영한다.
  • 쓰기 성능이 좋지만 캐시와 RAM이 일시적으로 불일치할 수 있다.
  • 이 불일치 상태의 라인을 더티(dirty) 라인이라고 한다.

현대 CPU는 대부분 Write-Back 방식을 사용한다.


캐시 일관성 (Cache Coherence)

멀티코어 CPU에서는 각 코어가 독립적인 L1/L2 캐시를 가진다. 같은 메모리 주소를 여러 코어가 각자의 캐시에 올렸을 때 한 코어가 값을 수정하면 다른 코어의 캐시가 오래된 값(stale data)을 갖게 된다.

이를 해결하기 위한 프로토콜이 MESI 프로토콜이다.

M (Modified)  : 이 코어 캐시만 최신 값, RAM과 불일치
E (Exclusive) : 이 코어 캐시만 보유, RAM과 일치
S (Shared)    : 여러 코어가 같은 값을 캐시에 보유
I (Invalid)   : 캐시 라인이 유효하지 않음

한 코어가 데이터를 수정하면 다른 코어의 해당 캐시 라인은 Invalid 상태로 전환된다. 다음에 그 코어가 데이터를 읽으려 하면 캐시 미스가 발생하고, 최신 값을 다시 가져온다.


캐시를 고려한 코드 작성

2D 배열 순회 — 행 우선 vs 열 우선

int arr[1000][1000];

// 좋은 예: 행 우선 순회 (공간적 지역성 활용)
for (int i = 0; i < 1000; i++)
    for (int j = 0; j < 1000; j++)
        sum += arr[i][j];

// 나쁜 예: 열 우선 순회 (캐시 미스 빈발)
for (int j = 0; j < 1000; j++)
    for (int i = 0; i < 1000; i++)
        sum += arr[i][j];

C/C++에서 2D 배열은 행(row) 단위로 메모리에 저장되므로, 열 우선 순회는 캐시 라인을 넘나들며 미스를 반복적으로 유발한다.

False Sharing

멀티스레드 환경에서 서로 다른 스레드가 다른 변수를 수정하더라도 그 변수들이 같은 캐시 라인 안에 있으면 캐시 무효화가 반복 발생한다.

// 문제: counter[0]과 counter[1]이 같은 캐시 라인에 위치할 수 있음
int counter[2];

// Thread 1: counter[0]++
// Thread 2: counter[1]++
// → 서로 관련 없지만 같은 캐시 라인이라 계속 무효화됨

이를 피하려면 패딩(padding)을 추가해 변수들을 서로 다른 캐시 라인에 배치한다.


정리

  • 캐시 메모리는 CPU와 RAM의 속도 차이를 줄이기 위한 계층적 구조다.
  • L1 < L2 < L3 순서로 크기가 커지고 속도가 느려진다.
  • 시간적 지역성, 공간적 지역성을 활용해 캐시 히트율을 높인다.
  • 캐시 라인 단위로 데이터를 가져오기 때문에, 메모리 접근 패턴이 성능에 직접 영향을 준다.
  • 멀티코어 환경에서는 MESI 같은 캐시 일관성 프로토콜로 코어 간 데이터 동기화를 유지한다.