사전 CPU 캐시
개념

CPU 캐시

gabury1고친 사람 github-actions[bot]

CPU 캐시는 프로세서가 방금 읽은 메모리 내용을 곁에 복사해 두고, 같은 내용을 다시 찾을 때 메모리까지 다녀오지 않게 해 줍니다. 프로세서는 메모리보다 훨씬 빨라서 값을 기다리는 동안 하는 일 없이 서 있습니다. 캐시는 그 기다리는 시간을 줄여 줍니다. 프로그램이 시키지 않아도 하드웨어가 알아서 채우고 비웁니다.

쉽고 빠른 이해

CPU 캐시는 프로세서와 메모리 사이에 끼어 자주 쓰는 내용을 대신 내주는 작은 저장 장치입니다. 프로세서가 메모리에 한 번 다녀오는 동안 명령을 여럿 처리할 만큼 둘의 속도가 벌어졌습니다.

메모리를 더 빠르게 만들면 되지 않느냐는 물음이 남습니다. 저장 장치는 속도를 올릴수록 같은 용량을 담는 데 훨씬 비쌉니다. 그래서 전부를 빠르게 만드는 대신 작은 장치 하나를 프로세서 곁에 둡니다.

어떻게 도는가:

  1. 프로세서가 어떤 주소의 값을 찾으면 캐시부터 뒤집니다
  2. 캐시에 있으면 그대로 내줍니다
  3. 없으면 메모리에서 그 주소 둘레를 한 덩어리로 가져와 캐시에 채우고 내줍니다

대가도 있습니다. 캐시에 든 것은 복사본이라 원본과 어긋날 수 있습니다. 프로세서 안에서 여러 갈래가 동시에 돌 때는 하드웨어가 복사본끼리 맞춰 줍니다. 그 맞추는 일에도 시간이 듭니다. 같은 코드도 데이터를 어떤 순서로 읽느냐에 따라 걸리는 시간이 갈립니다.

상세

엘리베이터는 마지막으로 사람이 내린 층에 그대로 서 있습니다. 누가 거기 세워 두라고 한 것이 아니라 방금 쓰인 자리에 그냥 남은 것입니다. 마침 그 층에서 버튼을 누른 사람은 기다릴 것 없이 바로 탑니다.

앞에서 말한 프로세서를 흔히 CPU(Central Processing Unit, 중앙처리장치)라고 부릅니다. 이 항목도 여기부터 CPU 라고 적습니다. 메모리는 CPU 가 지금 쓰는 명령어와 데이터를 담아 두는 저장 장치입니다.

CPU 캐시는 이 둘 사이에 끼어 있는 작은 저장 장치입니다. 메모리에서 읽어 온 내용의 복사본을 쥐고 있다가, 같은 내용을 다시 찾으면 메모리 대신 자기가 내줍니다. 웹 서버가 요청 하나를 처리하는 동안 같은 설정값과 같은 객체를 수십 번 들여다봅니다. 그런 값이 캐시에 들어앉습니다.

CPU 와 메모리의 속도 격차

CPU 는 해가 갈수록 빨라졌습니다. 메모리도 빨라졌지만 그만큼은 아니었습니다. 그래서 둘의 속도 차이가 점점 벌어졌습니다. 이 벌어진 차이를 메모리 벽이라고 부릅니다.

차이가 벌어지면 계산이 아무리 빨라도 값을 기다리는 시간이 전체를 정합니다. 명령 하나를 실행하는 시간보다 메모리에서 값을 한 번 가져오는 시간이 훨씬 깁니다. 그동안 CPU 는 할 일이 없어 멈춰 섭니다.

캐시는 이 격차를 없애지 못합니다. 메모리까지 가야 하는 횟수를 줄일 뿐입니다.

캐시 라인 — 한 번에 옮기는 덩어리

캐시는 값을 한 바이트씩 담지 않습니다. 메모리에서 가져올 때 그 주소 둘레의 이웃한 바이트까지 한 덩어리로 끌어옵니다. 이 덩어리를 캐시 라인이라고 부릅니다.

캐시가 담는 단위도 버리는 단위도 이 덩어리입니다. 라인 크기는 프로세서마다 다릅니다. 대개 수십 바이트라서 정수 몇 개나 짧은 문자열 하나는 통째로 들어옵니다.

그래서 값 하나를 읽으면 부르지도 않은 그 옆 값들이 이미 캐시에 와 있습니다.

캐시가 통하는 이유

프로그램이 메모리를 아무 데나 고루 읽는다면 캐시는 거의 맞지 않습니다. 실제 프로그램은 그렇게 돌지 않습니다. 방금 읽은 값을 곧 다시 읽고, 방금 읽은 값 바로 옆을 이어서 읽습니다. 이 성질을 참조 지역성이라고 합니다.

지역성은 두 갈래로 나눠 부릅니다.

갈래 뜻 흔한 예
시간 지역성 한 번 쓴 값을 곧 다시 쓴다 반복문 안에서 같은 변수를 계속 읽는다
공간 지역성 한 번 쓴 값의 이웃을 이어서 쓴다 배열을 앞에서 뒤로 훑는다

표의 두 갈래가 캐시의 두 설계와 짝을 이룹니다. 캐시 라인이 이웃까지 끌어오는 것은 공간 지역성을 노린 것입니다. 한 번 담은 라인을 얼마간 두고 보는 것은 시간 지역성을 노린 것입니다.

히트와 미스

찾는 값이 캐시에 있으면 캐시 히트입니다. 없으면 캐시 미스입니다. 미스가 나면 메모리에서 라인 하나를 가져와 캐시에 채운 다음 값을 내줍니다.

미스는 캐시가 고장 난 것이 아니라 캐시가 도는 방식의 한 부분입니다. 처음 읽는 데이터는 언제나 미스입니다. 그래서 성능을 볼 때 재는 것은 미스가 났느냐가 아니라, 전체 접근 가운데 히트가 차지하는 비율입니다.

층으로 나눈 캐시

캐시를 크게 만들면 더 많이 담지만 뒤지는 데 시간이 더 듭니다. 작게 만들면 뒤지기는 금세 끝나지만 금방 찹니다. 한 크기로 둘을 다 얻을 수 없어서 캐시를 여러 층으로 나눕니다.

안쪽 층일수록 작고 빠릅니다. 바깥쪽 층일수록 크고 느립니다. 실무에서는 안쪽부터 L1(Level 1) · L2(Level 2) · L3(Level 3) 이라고 부릅니다.

요즘 CPU 한 개에는 명령을 실행하는 단위가 여럿 들어 있습니다. 그 단위 하나를 코어라고 합니다. 아래 그림은 코어 하나가 값을 찾아 내려가는 길입니다.

flowchart TD
    C["코어"] --> A["L1 캐시 · 가장 작다"]
    A --> B["L2 캐시"]
    B --> D["L3 캐시 · 코어들이 함께 쓴다"]
    D --> M["메모리"]

그림에서 위로 올라갈수록 저장 장치가 작아지고 CPU 에 가까워집니다. 찾는 값이 L1 에 없으면 L2 로, 거기에도 없으면 L3 으로 내려가며 찾습니다. 마지막까지 없으면 메모리에서 가져옵니다.

L1 과 L2 는 대개 코어마다 하나씩 따로 있습니다. L3 은 한 칩에 든 코어들이 같이 씁니다. 저장 장치를 성격별로 이렇게 겹쳐 두는 구성을 메모리 계층이라고 합니다.

캐시가 다 차면 무엇을 버리나

캐시는 작아서 금방 찹니다. 새 라인을 넣으려면 들어 있던 라인 하나를 내보내야 합니다. 이렇게 내보내는 일을 축출이라고 합니다.

무엇을 내보낼지는 하드웨어가 정합니다. 흔한 기준은 가장 오래 안 쓰인 라인을 먼저 내보내는 것입니다. 한동안 안 쓴 것은 앞으로도 안 쓸 것이라고 보는 셈입니다.

값을 바꿀 때

읽기만 있으면 캐시는 단순합니다. 쓰기가 끼면 같은 내용이 캐시와 메모리 두 곳에 있게 되어 어느 쪽이 옳은지 정해야 합니다.

캐시에 쓰면서 메모리에도 같이 쓰는 방식을 write-through라고 합니다. 캐시에만 써 두고 그 라인이 밀려날 때 메모리로 내려보내는 방식을 write-back이라고 합니다. CPU 캐시는 메모리를 덜 건드리는 뒤쪽을 흔히 씁니다.

뒤쪽 방식을 쓰면 메모리에 아직 안 내려간 라인이 생깁니다. 그런 라인에는 바뀌었다는 표시를 달아 둡니다. 밀려날 때 이 표시를 보고 메모리에 적을지 그냥 버릴지 정합니다.

코어가 여럿일 때

코어마다 자기 캐시를 들고 있습니다. 같은 주소를 두 코어가 각자 캐시에 담고 있다가 한쪽이 값을 바꾸면, 다른 쪽이 든 복사본은 낡은 값이 됩니다.

그래서 코어들은 서로의 캐시 상태를 주고받으며 맞춥니다. 이 맞추는 규칙을 캐시 일관성이라고 합니다. 한 코어가 어떤 라인을 고치면 같은 라인을 들고 있던 다른 코어의 복사본은 무효가 됩니다.

여기서 거짓 공유가 나옵니다. 서로 다른 변수가 같은 캐시 라인에 얹혀 있으면, 각자 자기 변수만 고쳐도 라인 전체가 코어 사이를 오갑니다. 스레드를 늘려도 속도가 그만큼 안 오르는 원인 가운데 하나입니다.

코드가 캐시를 타는 방식

백엔드 코드에서 캐시를 직접 다루는 명령을 쓸 일은 드뭅니다. 대신 데이터를 어떤 순서로 놓고 읽느냐가 히트와 미스를 정합니다. 이차원 배열을 훑는 두 코드를 견줘 봅니다.

Java
for (int i = 0; i < n; i++)
  for (int j = 0; j < n; j++)
    sum += a[i][j];  // 이웃 칸을 차례로

자바에서 이차원 배열은 행 하나가 메모리에 이어져 놓입니다. 위 코드는 그 이어진 순서대로 읽으므로, 라인 하나를 가져오면 그 안에 든 칸을 남김없이 씁니다.

Java
for (int j = 0; j < n; j++)
  for (int i = 0; i < n; i++)
    sum += a[i][j];  // 행을 건너뛰며

아래 코드는 칸 하나를 읽을 때마다 다른 행으로 건너뜁니다. 가져온 라인에서 한 칸만 쓰고 버리는 셈이라 미스가 잦아집니다. 두 코드의 계산 결과는 같습니다. 읽는 순서만 다릅니다.

언제 신경 쓰고 언제 잊어도 되나

캐시는 켜고 끄는 것이 아닙니다. 프로그램이 도는 내내 하드웨어가 쓰고 있습니다. 그래서 대부분의 코드는 캐시를 몰라도 돌아갑니다.

따져 볼 만한 때는 정해져 있습니다. 데이터가 많고, 그것을 되풀이해 훑고, 훑는 일이 전체 시간의 큰 몫을 차지할 때입니다. 요청 하나가 데이터베이스나 네트워크를 기다리는 서비스라면 기다리는 쪽이 훨씬 커서, 캐시를 따져도 줄어드는 시간이 눈에 안 띕니다.

관련 항목

CPU 캐시와 함께 메모리 계층을 이루는 저장 장치

레지스터 · 메모리 · 메모리 계층 · 디스크 · SSD · 메모리 벽

CPU 캐시를 이루는 구성 요소

캐시 라인 · 캐시 태그 · 캐시 집합 · SRAM · 더티 비트

캐시를 뒤진 결과와 그 결과를 재는 지표

캐시 히트 · 캐시 미스 · 적중률 · 콜드 미스 · 충돌 미스 · 용량 미스

CPU 캐시가 기대는 프로그램의 성질

참조 지역성 · 시간 지역성 · 공간 지역성 · 선인출 · 순차 접근 · 임의 접근

캐시에 무엇을 남기고 무엇을 내보낼지 정하는 방식

축출 · LRU · write-back · write-through · 캐시 무효화 · 집합 연관

코어가 여럿일 때 생기는 문제와 그것을 맞추는 규칙

캐시 일관성 · MESI · 거짓 공유 · 메모리 장벽 · 메모리 모델 · 원자적 연산

CPU 캐시를 품는 장치

CPU · 코어 · 멀티코어 · NUMA · CPU 파이프라인

주소를 옮기는 동안 캐시와 함께 도는 장치

TLB · MMU · 가상 메모리 · 물리 주소 · 페이지

같은 원리를 소프트웨어로 옮긴 캐시

캐싱 · 페이지 캐시 · 버퍼 풀 · 브라우저 캐시 · CDN · 캐시 스탬피드

다른 이름: CPU cache · 캐시 메모리 · cache memory · 프로세서 캐시