2026년 02월 21일

🚀 CPU는 왜 이렇게 빠를까?

Java Spring Boot
Cover Image

🚀 CPU는 왜 이렇게 빠를까?

— 캐시(Cache) 완전 정복

핵심 요약: CPU는 초당 수십억 번 연산하지만, RAM은 그 속도를 따라가지 못한다.
이 병목을 해결하기 위해 CPU 가까이 배치한 초고속 임시 저장소가 캐시(Cache) 다.


1️⃣ 캐시가 탄생한 이유

CPU와 RAM 사이에는 심각한 속도 격차가 있다.

CPU 연산:  ~0.3 나노초
RAM 접근: ~100 나노초  →  CPU 입장에서 300배 넘게 기다려야 함

CPU가 연산할 데이터를 RAM에서 가져오는 동안 아무것도 못 하고 멈추는 상태를 스톨(Stall) 이라고 한다.
캐시는 이 스톨을 최소화하기 위해 자주 쓰는 데이터를 CPU 가까이 미리 복사해두는 전략이다.

🧠 캐시가 효과적인 이유 — 지역성(Locality)

캐시 전략이 효과적인 이유는 프로그램의 데이터 접근 패턴에 규칙성이 있기 때문이다.

지역성 종류의미예시
시간 지역성 (Temporal)한 번 쓴 데이터는 곧 또 쓴다루프 내 변수, 카운터
공간 지역성 (Spatial)방금 쓴 데이터 주변도 곧 쓴다배열 순차 접근, 구조체 필드
// 두 지역성이 모두 나타나는 전형적인 예
int[] arr = new int[1000];
for (int i = 0; i < arr.length; i++) {  // i → 시간 지역성
    arr[i] += 1;                          // arr[i] → 공간 지역성
}

2️⃣ L1 / L2 / L3 캐시 계층

캐시는 속도와 용량에 따라 3단계로 나뉜다.
숫자가 작을수록 CPU에 가깝고 빠르지만 용량은 작다.

계층별 비교

구분위치용량속도공유 여부
L1코어 내부수십 ~ 수백 KB가장 빠름 (~1ns)코어 전용
L2코어 근처수백 KB ~ 수 MB빠름 (~5ns)코어 전용
L3CPU 칩 내부수 MB ~ 수십 MB보통 (~20ns)모든 코어 공유
RAM메인보드수 GB ~ 수십 GB느림 (~100ns)시스템 전체

💡 L1이 명령어(i)와 데이터(d)로 분리된 이유?
명령어를 읽는 동시에 데이터를 읽을 수 있어 파이프라인 효율이 올라가기 때문이다.


3️⃣ 캐시 히트와 캐시 미스

CPU가 데이터를 요청할 때 두 가지 결과가 나온다.

캐시 히트율(Cache Hit Rate) 이 높을수록 프로그램이 빠르다.
실제 잘 작성된 프로그램의 L1 캐시 히트율은 95% 이상에 달한다.


4️⃣ Java 개발자 관점 — 캐시 친화적 코드

📌 배열 vs 리스트: 캐시 효율 차이

// ✅ 캐시 친화적 — 메모리에 연속 배치
int[] arr = new int[1000];
for (int i = 0; i < arr.length; i++) {
    arr[i] += 1;  // 공간 지역성 → 캐시 히트율 높음
}

// ⚠️ 캐시 비친화적 — 객체가 Heap 곳곳에 흩어짐
List<Integer> list = new ArrayList<>();
for (Integer num : list) {
    // 각 Integer 객체 참조마다 Heap의 다른 위치 접근 → 캐시 미스 빈번
}
int[] 배열 메모리 구조 (연속):
[1][2][3][4][5][6]...  → 캐시 라인(64byte)에 여러 원소가 한번에 올라옴

ArrayList<Integer> 메모리 구조 (분산):
[ref1] → Heap:0xA1 [1]
[ref2] → Heap:0xF3 [2]  ← 각 Integer 객체가 Heap 여기저기 산재
[ref3] → Heap:0x2B [3]

⚡ False Sharing (거짓 공유)

멀티스레드 환경에서 의도치 않게 성능이 떨어지는 대표적인 함정이다.

캐시 라인 (64 bytes) Thread A가 counter1을 수정하면 → 캐시 라인 전체가 무효화 → Thread B도 같은 캐시 라인을 다시 읽어야 함 → 성능 저하

// ❌ False Sharing 발생 — 두 변수가 같은 캐시 라인에 위치할 수 있음
class Shared {
    volatile long counter1 = 0;
    volatile long counter2 = 0;
}

// ✅ Java 8+ @Contended 어노테이션으로 해결 (패딩 삽입)
class Padded {
    @jdk.internal.vm.annotation.Contended
    volatile long counter1 = 0;
    @jdk.internal.vm.annotation.Contended
    volatile long counter2 = 0;
}

💡 2D 배열 순회 방향도 성능에 영향을 준다

int[][] matrix = new int[1000][1000];

// ✅ 행 우선 순회 (Row-major) — 공간 지역성 활용, 빠름
for (int i = 0; i < 1000; i++)
    for (int j = 0; j < 1000; j++)
        matrix[i][j]++;

// ❌ 열 우선 순회 (Column-major) — 캐시 라인을 건너뜀, 느림
for (int j = 0; j < 1000; j++)
    for (int i = 0; i < 1000; i++)
        matrix[i][j]++;

같은 Big-O라도 캐시 친화적 코드가 수십 배 빠를 수 있다.
알고리즘 복잡도가 같다면 데이터 접근 패턴이 실제 성능을 결정한다.


5️⃣ 핵심 정리

개념한 줄 요약
캐시CPU-RAM 속도 격차를 줄이는 고속 임시 저장소
지역성시간(재사용) + 공간(주변 접근) 패턴을 캐시가 활용
L1/L2코어 전용, 빠를수록 작음
L3모든 코어 공유, L1/L2 미스 시 최후 방어선
캐시 히트율높을수록 좋음, 연속 메모리 접근이 핵심
False Sharing다른 변수가 같은 캐시 라인에 있으면 멀티스레드 성능 저하
배열 vs 리스트int[]ArrayList<Integer>보다 캐시 효율이 훨씬 좋음
← 목록으로 돌아가기