메모리와 캐시
CPU가 아무리 빨라도 재료가 늦게 오면 기다릴 수밖에 없다. 1장에서 CPU의 한 박자를 1초로 늘리면 메모리까지 다녀오는 데 5분이 걸린다고 했다. 이 장은 그 5분을 어떻게 숨기는지에 대한 이야기다. 핵심 단어는 하나, “가까운 곳에 두기”다.
- 메모리를 “번호(주소)가 붙은 사물함”으로 이해하고 주소와 값을 구분한다.
- DRAM이 전원이 꺼지면 지워지고, 켜져 있어도 계속 새로고침해야 하는 이유를 안다.
- 레지스터 → 캐시 → RAM → SSD로 이어지는 메모리 계층의 크기·속도·가격 관계를 설명한다.
- 캐시 적중과 실패를 시뮬레이터로 따라가며 평균 접근 시간을 계산한다.
- 시간 지역성과 공간 지역성이 무엇이며, 코드 작성 방식이 성능을 바꾸는 이유를 안다.
메모리는 번호 붙은 사물함
메모리(RAM)는 아주 긴 사물함 줄이라고 생각하면 된다. 사물함 한 칸에는 1바이트(8비트)가 들어가고, 칸마다 0번부터 시작하는 번호가 붙어 있다. 이 번호를 주소(address)라 한다. 16 GB 메모리라면 사물함이 약 160억 칸이다.
CPU는 “1000번지 값을 줘” 또는 “1000번지에 65를 넣어”라고만 말한다. 주소만 알면 몇 번째 칸이든 똑같이 빠르게 접근할 수 있어서 RAM(Random Access Memory, 아무 곳이나 바로 접근하는 메모리)이라는 이름이 붙었다. 처음부터 차례로 감아야 하는 카세트테이프와 대비되는 이름이다.
메모리 사물함: 주소와 값
주소는 메모리 칸의 번호이고, 이 번호 자체도 다른 칸에 숫자로 저장할 수 있다. “사물함 3번 안에 ‘17번 사물함을 열어 봐’라는 쪽지” 같은 것이다. 이 단순한 아이디어로 목록, 트리, 웹 페이지의 링크 구조 같은 복잡한 데이터를 엮는다.
RAM의 정체: 새는 물동이
3장의 래치는 트랜지스터 여러 개로 1비트를 기억했다. 빠르지만 자리를 많이 차지한다. 메인 메모리로 쓰는 DRAM(Dynamic RAM)은 1비트를 트랜지스터 1개 + 아주 작은 전기 그릇(커패시터) 1개로 기억한다. 그릇에 전하가 차 있으면 1, 비어 있으면 0이다. 덕분에 아주 촘촘하게, 싸게 만들 수 있다.
문제는 이 그릇이 조금씩 샌다는 것이다. 가만히 두면 몇십 밀리초 안에 1이 0처럼 보일 만큼 전하가 빠져나간다. 그래서 메모리 칩은 모든 칸을 주기적으로 읽어서 다시 채워 넣는 리프레시(refresh)를 끊임없이 한다. 보통 64 ms마다 한 바퀴를 돈다. 이름의 “Dynamic(동적)”이 바로 이 쉼 없는 새로고침을 뜻한다.
DRAM 셀의 누설과 리프레시
CPU 안의 캐시와 레지스터는 새지 않는 래치 방식의 SRAM(Static RAM)을 쓴다. 리프레시가 필요 없고 훨씬 빠르지만, 비트당 트랜지스터가 6개 정도 들어서 같은 면적에 담을 수 있는 양이 적고 비싸다. 빠른 것은 작고 비싸며, 큰 것은 느리고 싸다. 이 맞바꿈이 다음 절의 메모리 계층을 만든다.
메모리 계층: 가까울수록 빠르고 작다
이상적인 메모리는 “무한히 크고, 무한히 빠르고, 공짜”일 것이다. 그런 것은 없다. 그래서 컴퓨터는 성격이 다른 메모리 여러 층을 쌓아 쓴다. 위로 갈수록 빠르고 작고 비싸며, CPU와 가깝다.
요리사는 지금 쓰는 재료를 손(레지스터)에 쥐고, 자주 쓰는 양념은 손 닿는 선반(L1), 조금 덜 쓰는 것은 뒤쪽 선반(L2·L3), 오늘 쓸 재료 전체는 조리대(RAM), 나머지는 창고(SSD)에 둔다. 선반에서 꺼내는 데 1초, 조리대까지 몇 분, 창고까지는 며칠이 걸린다고 생각하면 1장의 시간 변환기와 맞아떨어진다. 좋은 요리사는 다음에 쓸 재료를 미리 선반으로 옮겨 둔다.
캐시: 다음에 쓸 것을 옆에 두기
캐시(cache)는 메인 메모리의 일부 내용을 복사해 CPU 바로 옆에 둔 작고 빠른 메모리다. CPU가 어떤 주소의 데이터를 요청하면 먼저 캐시를 본다.
- 캐시 적중(hit): 캐시에 있다! 1~4 ns 만에 가져온다.
- 캐시 실패(miss): 없다. 메인 메모리까지 가서 가져오고(약 100 ns), 다음을 위해 캐시에도 복사해 둔다. 캐시가 꽉 차 있으면 기존 내용 하나를 내보낸다.
아래 시뮬레이터의 캐시는 8칸짜리다. 메모리 주소마다 들어갈 수 있는 캐시 칸이 정해져 있다(주소를 8로 나눈 나머지). 이런 방식을 직접 사상 캐시(direct-mapped cache)라 한다. 접근 패턴을 바꿔 가며 적중률이 어떻게 달라지는지 보자.
8칸 캐시 시뮬레이터
적중률이 조금만 달라져도 평균 속도는 크게 달라진다. 적중률이 99%면 평균 약 2 ns이지만, 90%면 약 11 ns로 다섯 배 넘게 느려진다. 실제 프로그램은 대부분 L1 캐시에서 95% 이상 적중한다. 어떻게 그렇게 잘 맞힐까? 비결은 프로그램의 습관, 즉 지역성에 있다.
지역성: 프로그램의 습관
프로그램은 메모리를 아무렇게나 쓰지 않는다. 두 가지 뚜렷한 습관이 있다.
- 시간 지역성: 방금 쓴 것은 곧 또 쓴다. 반복문 안의 변수, 자주 부르는 함수가 그렇다. (“방금 쓴 소금은 곧 또 쓴다”)
- 공간 지역성: 방금 쓴 것 근처를 곧 쓴다. 배열을 처음부터 차례로 읽는 경우가 그렇다. (“양파를 꺼냈으면 옆의 마늘도 곧 쓴다”)
캐시는 공간 지역성을 이용하려고 한 바이트만 가져오지 않고 주변 64바이트를 한꺼번에 가져온다. 이 묶음을 캐시 라인(cache line)이라 한다. 그래서 데이터를 메모리에 놓인 순서대로 읽으면 빠르고, 띄엄띄엄 건너뛰며 읽으면 느리다. 같은 계산을 하는데도 말이다.
표를 읽는 두 가지 순서
표 한 줄이 메모리에 가로로 이어져 저장된다. 캐시 라인 = 가로 4칸, 캐시 크기 = 라인 6개.
게임 엔진, 데이터베이스, 인공지능 라이브러리를 만드는 개발자들은 데이터를 메모리에 어떤 순서로 둘지 신중하게 설계한다. 계산 횟수를 줄이는 것만큼 캐시 실패를 줄이는 것이 중요하기 때문이다. 5장의 분기 예측과 함께, “같은 결과, 다른 속도”를 만드는 대표적인 이유다.
핵심 정리
- 메모리는 1바이트짜리 칸이 줄지은 사물함이다. 칸 번호가 주소이고, 주소 자체도 숫자로 저장할 수 있다(포인터).
- DRAM은 커패시터에 전하를 담아 기억한다. 전하가 새므로 계속 리프레시해야 하고, 전원이 꺼지면 내용이 사라진다.
- 빠른 메모리는 작고 비싸다. 그래서 레지스터 → L1/L2/L3 캐시 → DRAM → SSD의 계층으로 쌓아 쓴다.
- 캐시는 최근·근처 데이터를 CPU 옆에 복사해 둔다. 적중하면 빠르고, 실패하면 메모리까지 가야 한다.
- 프로그램의 시간·공간 지역성 덕분에 캐시가 잘 맞는다. 메모리에 놓인 순서대로 읽는 코드가 빠르다.
확인 퀴즈
RAM이 전원이 꺼지면 내용을 잃는 이유로 가장 알맞은 것은?
메모리 계층에서 위(CPU 쪽)로 갈수록 어떻게 되는가?
적중률 90%, 적중 시 1 ns, 실패 시 100 ns라면 평균 접근 시간은?
배열을 처음부터 끝까지 차례로 읽을 때 캐시가 잘 맞는 이유는?