본문 바로가기

메모리 계층과 주기억장치

속도·용량·비용을 함께 만족하는 기억장치가 없어서 생긴 메모리 계층과 지연·대역폭의 차이를 설명하고, SRAM·DRAM·ROM·플래시를 비교합니다. 주소선과 데이터 폭으로 칩의 용량을 계산하고, 지역성이 계층을 쓸모 있게 만드는 이유와 평균 접근 시간을 코드로 확인합니다.

CPU 바로 옆에 모든 프로그램과 사진, 영상을 담을 만큼 빠른 기억장치를 두면 좋겠지만, 그런 기억장치는 너무 비싸고 전력도 많이 씁니다. 반대로 값싸고 큰 기억장치는 CPU가 기다리기에는 너무 느립니다. 그래서 컴퓨터는 작고 빠른 기억장치와 크고 느린 기억장치를 여러 층으로 쌓고, 자주 쓰는 데이터를 위층에 복사해 둡니다. 이 구조를 메모리 계층(memory hierarchy)이라고 합니다. 이 글에서는 계층의 각 층과 주기억장치를 이루는 소자를 살펴보고, 계층이 왜 효과가 있는지 다룹니다.

위로 갈수록 작고 빠르다

메모리 계층은 위에서부터 레지스터, 캐시, 주기억장치, 저장장치 순서로 놓입니다. 위층일수록 CPU에 가깝고, 작고, 빠르며, 비트당 값이 비쌉니다. 각 층의 대략적인 크기는 다음과 같은데, 실제 값은 부품과 설계, 접근 방식에 따라 크게 달라집니다.

계층대략적인 용량대략적인 접근 시간
레지스터수백 바이트에서 수 KiB1 ns 미만
캐시(SRAM)수십 KiB에서 수십 MiB1 ns에서 수십 ns
주기억장치(DRAM)수 GiB에서 수백 GiB수십 ns에서 100 ns 안팎
SSD수백 GB에서 수 TB수십에서 수백 µs
하드 디스크수 TB 이상수 ms

기억장치의 빠르기는 두 가지로 나누어 보아야 합니다. 접근 지연(latency)은 요청 하나가 끝날 때까지 걸리는 시간이고, 대역폭(bandwidth)은 정해진 시간에 옮길 수 있는 데이터의 양입니다. 첫 데이터가 늦게 도착하더라도 그 뒤로 큰 덩어리를 빠르게 옮기는 장치가 있으므로, 둘을 "속도" 하나로 뭉뚱그리면 장치를 잘못 비교하게 됩니다.

각 층은 아래층의 일부를 복사해 둡니다. CPU의 LOAD 명령어는 주기억장치의 주소에 접근할 뿐, 저장장치의 파일을 바로 읽지는 않습니다. 파일의 내용이 필요하면 운영체제가 장치를 제어해 그 내용을 주기억장치로 옮긴 뒤에야 CPU가 읽을 수 있습니다. 1장의 프로그램이 하드웨어를 움직이는 과정에서 본 메모리와 파일의 차이가 바로 이것입니다.

SRAM, DRAM, ROM, 플래시

SRAM(static RAM)은 트랜지스터 여러 개로 만든 기억 회로에 비트를 담아, 전원이 들어와 있는 동안 값을 그대로 유지합니다. 빠르지만 비트 하나에 트랜지스터가 여러 개 필요해 비싸므로 캐시에 씁니다. DRAM(dynamic RAM)은 비트 하나를 작은 축전기의 전하로 담는데, 전하가 조금씩 새어 나가므로 주기적으로 다시 채우는 재생(refresh)을 해야 합니다. 구조가 단순해 싸고 집적도가 높아서 주기억장치로 씁니다. 이름의 static과 dynamic은 소자가 값을 유지하는 방식을 말하며, 프로그램의 정적, 동적과는 상관이 없습니다.

SRAM과 DRAM은 전원이 꺼지면 내용이 사라지는 휘발성 기억장치입니다. 전원이 꺼져도 내용이 남는 비휘발성 기억장치로는 ROM 계열과 플래시가 있습니다. 만들 때 내용을 고정한 ROM, 한 번만 쓸 수 있는 PROM, 지우고 다시 쓸 수 있는 EPROM과 EEPROM, 큰 블록 단위로 지운 뒤 그보다 작은 단위로 쓰는 플래시 메모리(flash memory)는 쓰고 지우는 방법이 서로 다릅니다. SSD와 USB 메모리가 플래시를 씁니다. RAM(random access memory)이라는 이름은 아무 위치나 같은 방식으로 접근할 수 있다는 뜻이라서, 휘발성인지는 이름이 아니라 소자의 종류로 판단해야 합니다.

주소선과 데이터 폭으로 용량을 계산한다

메모리 칩의 주소가 k비트이면 구별할 수 있는 위치가 2k2^k개이고, 위치마다 w비트를 담으면 칩의 용량은 2k×w2^k\times w비트입니다. 주소를 한 번에 모두 보내는 칩이라면 주소선의 개수가 곧 k입니다. 주소선 12개에 데이터 폭이 8비트이면 212×82^{12}\times8비트, 곧 4096바이트입니다.

DRAM은 핀의 수를 줄이려고 같은 주소선으로 행(row) 주소와 열(column) 주소를 서로 다른 시점에 나누어 보내는데, 이것을 주소 다중화(address multiplexing)라고 합니다. 주소선 6개를 행 6비트와 열 6비트로 두 번 쓰면 26×26=2122^6\times2^6=2^{12}개의 위치를 구별하므로, 핀의 수만 세어 262^6개라고 계산하면 틀립니다. 실제 DRAM 칩의 행, 열, 뱅크 구성과 명령, 타이밍은 데이터시트에 정해진 조건을 따릅니다. 여러 칩을 묶어 데이터 폭과 주소 범위를 늘리는 방법은 메모리 칩의 폭·뱅크·선택 신호에서 다룹니다.

지역성이 계층을 쓸모 있게 만든다

위층에 복사해 둔 데이터가 쓸모 있으려면, 프로그램이 같은 데이터나 가까운 데이터를 다시 써야 합니다. 실제로 많은 프로그램이 이런 경향을 보이는데, 이것을 지역성(locality)이라고 합니다. 최근에 쓴 데이터를 곧 다시 쓰는 경향을 시간 지역성(temporal locality), 방금 쓴 주소의 가까운 주소를 곧 쓰는 경향을 공간 지역성(spatial locality)이라고 합니다. 반복문이 같은 명령어와 합계 변수를 거듭 쓰는 것은 시간 지역성이고, 배열을 앞에서부터 차례로 읽는 것은 공간 지역성입니다.

캐시는 공간 지역성을 이용하려고 데이터를 한 바이트씩이 아니라 블록(block) 단위로 가져옵니다. 블록 하나를 가져오면 그 안의 이웃한 데이터도 함께 들어오므로, 다음 접근이 그 블록 안이라면 다시 아래층에 갈 필요가 없습니다. 반대로 접근이 여기저기 흩어져 있으면 블록을 가져와도 거의 쓰지 못합니다. 모든 프로그램이 지역성이 강한 것은 아니며, 같은 일을 하는 코드라도 접근 순서를 바꾸어 지역성을 높일 수 있습니다.

평균 접근 시간

캐시가 하나인 가장 단순한 모델에서 평균 메모리 접근 시간(average memory access time, AMAT)은 적중 시간(hit time) + 실패율(miss rate) × 실패 손실(miss penalty)입니다. 여기서 실패 손실은 캐시를 확인한 뒤에 더 드는 시간입니다. 적중 시간이 2 ns, 실패율이 5%, 실패 손실이 80 ns이면 2 + 0.05 × 80 = 6 ns입니다.

def amat(hit_time, miss_rate, miss_penalty):
    return hit_time + miss_rate * miss_penalty


for hit, rate in ((2, 0.05), (2, 0.03), (4, 0.03)):
    print(hit, rate, round(amat(hit, rate, 80), 2))
print(round(amat(1, 0.05, amat(10, 0.2, 100)), 2))


def blocks_touched(addresses, block_size=16):
    return len({a // block_size for a in addresses})


print(blocks_touched([4 * i for i in range(16)]), blocks_touched([64 * i for i in range(16)]))
2 0.05 6.0
2 0.03 4.4
4 0.03 6.4
2.5
4 16

처음 세 줄에서 실패율을 5%에서 3%로 낮추면 평균이 4.4 ns로 줄지만, 그 대가로 적중 시간이 4 ns로 늘면 오히려 6.4 ns가 됩니다. 실패율만 보고 캐시가 좋아졌다고 판단하면 안 되는 이유입니다. 넷째 줄은 캐시가 두 단계인 경우로, 첫 캐시의 실패 손실 자리에 둘째 캐시의 평균 접근 시간을 넣었습니다. 첫 캐시는 적중 시간 1 ns, 실패율 5%이고, 둘째 캐시는 적중 시간 10 ns, 실패율 20%, 주기억장치까지 100 ns라서 1 + 0.05 × (10 + 0.2 × 100) = 2.5 ns입니다.

마지막 줄은 공간 지역성의 효과입니다. 4바이트 원소 16개를 차례로 읽으면 16바이트 블록 4개만 가져오면 되지만, 64바이트 간격으로 16번 읽으면 접근할 때마다 다른 블록이라 16개를 모두 가져와야 합니다. 읽는 데이터의 양은 같아도 아래층에서 가져와야 하는 횟수는 네 배 차이가 납니다.

정리

  • 메모리 계층은 레지스터, 캐시, 주기억장치, 저장장치 순서로 놓이며, 위층일수록 작고 빠르고 비쌉니다.
  • 접근 지연은 요청 하나의 시간, 대역폭은 정해진 시간에 옮기는 양으로, 서로 다른 빠르기입니다.
  • SRAM은 재생이 필요 없어 빠르고 비싸서 캐시에, DRAM은 재생이 필요하지만 싸고 집적도가 높아 주기억장치에 쓰이며, ROM 계열과 플래시는 비휘발성입니다.
  • 주소가 k비트이고 위치마다 w비트이면 용량은 2k×w2^k\times w비트이며, 주소 다중화를 쓰는 칩은 핀의 수만으로 주소 폭을 셀 수 없습니다.
  • 시간 지역성과 공간 지역성이 계층을 쓸모 있게 만들고, 평균 접근 시간은 적중 시간 + 실패율 × 실패 손실입니다.

연습 문제

1. 실패 손실의 정의

캐시 A는 적중 시간 1 ns, 실패율 10%, 실패 손실 50 ns이고, 캐시 B는 적중 시간 2 ns, 실패율 5%, 실패 손실 50 ns입니다. 두 캐시의 평균 접근 시간을 구하세요. 제조사가 A에 대해 "실패했을 때 전체 51 ns"라고 적었다면, 이 51을 실패 손실 자리에 그대로 넣어도 되나요?

해설 보기

A는 1 + 0.1 × 50 = 6 ns, B는 2 + 0.05 × 50 = 4.5 ns로, 적중 시간이 더 긴 B가 평균은 더 빠릅니다. 실패했을 때 전체 51 ns라는 값에는 캐시를 확인한 1 ns가 이미 들어 있으므로, 실패 손실은 51 − 1 = 50 ns입니다. 51을 그대로 넣으면 적중 시간을 두 번 더하게 됩니다. 시간 값을 쓸 때는 그것이 전체 시간인지 더 드는 시간인지부터 확인해야 합니다.

2. 칩의 용량

주소를 한 번에 보내는 칩에 주소선이 14개 있고 데이터 폭이 16비트이면 용량은 얼마인가요? 주소선 10개로 행 주소와 열 주소를 나누어 보내는 DRAM 칩은 위치를 몇 개 구별할 수 있나요?

해설 보기

첫 칩은 214=163842^{14}=16384개 위치에 16비트씩이므로 214×162^{14}\times16비트, 곧 2152^{15}바이트 = 32 KiB입니다. DRAM 칩은 주소선 10개를 행 10비트와 열 10비트로 두 번 쓰므로 210×210=2202^{10}\times2^{10}=2^{20}, 곧 약 100만 개의 위치를 구별합니다. 주소선 10개만 보고 210=10242^{10}=1024개라고 하면 주소 다중화를 놓친 것입니다.

3. 이차원 배열을 읽는 순서

행 우선(row-major)으로 저장된 1000 × 1000 배열의 모든 원소를 더할 때, 행을 따라 읽는 반복과 열을 따라 읽는 반복 가운데 어느 쪽이 캐시를 더 잘 쓰나요?

해설 보기

행 우선 배열은 같은 행의 원소들이 메모리에 이어져 있으므로, 행을 따라 읽으면 다음 원소가 바로 옆 주소에 있어 공간 지역성이 좋습니다. 블록 하나를 가져오면 그 안의 여러 원소를 차례로 씁니다. 열을 따라 읽으면 다음 원소가 한 행 길이만큼 떨어진 주소에 있어, 접근할 때마다 다른 블록을 가져와야 할 수 있습니다. 더하는 원소와 결과는 같지만, 행을 따라 읽는 쪽이 아래층에 가는 횟수가 훨씬 적어 빠릅니다.

캐시의 성능을 비교할 때는 실패율 하나만 보지 말고 적중 시간과 실패 손실을 함께 넣어 계산해 보세요. 지역성은 MIT 6.004의 메모리 계층 강의에서도 더 볼 수 있습니다. 다음 글 캐시 주소·매핑·교체에서는 주소로 캐시의 어느 자리를 볼지 정하는 방법을 다룹니다.