# 현대 프로세서의 메모리 계층 구조: 레지스터부터 DRAM까지
현대 시스템에서 캐시 미스는 100–200 사이클, 히트는 1–4 사이클이 소요됩니다. 이는 코드 성능을 좌우합니다. 실제 사례를 보죠: 1 GHz RISC-V에서 네트워크 인터페이스 드라이버를 최적화한 경우. 예상 패킷 처리 속도는 패킷당 500 지시어(500 ns)로 초당 200만 개였습니다. 실제 달성 속도는 초당 20만 개.
프로파일링 결과:
$ perf stat -e cycles,instructions,cache-misses ./driver_test
Performance counter stats:
5,000,000 cycles
500,000 instructions
45,000 cache-misses
50만 지시어는 IPC=1에서 50만 사이클이 필요하지만, 실제로는 500만 사이클이 소요되었습니다. 차이는 4만5천 미스 × 100 사이클 = 450만 사이클입니다. 계산에 10%, 메모리 대기에 90%가 걸렸습니다.
메모리 계층 구조
메모리는 속도가 점차 느려지고 크기가 커지는 계층 구조로 조직되어 있습니다:
| 계층 | 유형 | 지연 시간 | 크기 |
|---------|-----|----------|--------|
| 레지스터 | 32 레지스터 | 1 사이클 | ~128 B |
| L1 캐시 | 지시어/데이터 | 3–4 사이클 | 32–64 KB |
| L2 캐시 | 통합 | 12–15 사이클 | 256–512 KB |
| L3 캐시 | 공유 | 40–50 사이클 | 2–32 MB |
| DRAM | 메인 | 100–200 사이클 | GB–TB |
주요 특성:
- 속도가 1에서 200 사이클로 급감.
- 크기가 128 B에서 TB로 폭증.
- DRAM은 L1보다 100–200배 느림.
마이크로컨트롤러(RISC-V RV32IMC, 100 MHz)에서는 계층이 단순화됩니다:
| 계층 | 유형 | 지연 시간 | 크기 |
|---------|-----|----------|--------|
| 레지스터 | 32 레지스터 | 1 사이클 | 128 B |
| I-캐시 L1 | 지시어 | 1 사이클 | 16 KB |
| D-캐시 L1/SRAM | 데이터 | 1–2 사이클 | 8–32 KB |
| 플래시 | 코드 | ~10 사이클 | 128 KB–1 MB |
| DRAM | 데이터 | 50–100 사이클 | 8–64 MB |
차이점: 캐시가 작고 L2/L3 없음, 코드에 DRAM 대신 플래시 사용, 크기 제한적.
캐시 라인과 데이터 액세스
캐시는 64바이트 블록 단위로 작동합니다. 1바이트를 액세스해도 전체 라인을 로드합니다.
int 액세스 예제:
int x = array[0]; // 4 bytes at 0x1000
// Loads 64 bytes: 0x1000–0x103F (16 ints)
순차 액세스는 효율적:
for (int i = 0; i < 16; i++) {
sum += array[i]; // 1 miss, 15 hits
}
랜덤 액세스는 비효율적:
for (int i = 0; i < 16; i++) {
sum += array[random_index[i]]; // Many misses
}
캐시 조직: 세트와 웨이
캐시는 세트와 웨이로 나뉩니다. 직접 매핑:
주소 비트: [Tag | Index | Offset]
32 KB, 64 B/라인 기준: 512 라인, 9비트 인덱스, 6비트 오프셋, 17비트 태그(32비트 주소).
충돌: 배열 a[1024](0x10000)과 b[1024](0x18000)이 동일 세트에 매핑.
세트 결합식(N-웨이): 세트에 N개 라인 포함, 충돌 감소.
일반적인 매개변수:
- L1: 8-웨이, 32–64 KB.
- L2: 8–16-웨이, 256–512 KB.
- L3: 16-웨이, 2–32 MB.
임베디드: 직접 또는 2-웨이, 충돌 확률 높음.
액세스 지역성
공간적: 인접 주소.
좋음:
for (int i = 0; i < n; i++) sum += array[i];
나쁨:
for (int i = 0; i < n; i++) sum += array[random[i]];
시간적: 반복 액세스.
좋음:
int temp = array[0];
for (int i = 0; i < 1000; i++) result += temp * i;
나쁨:
for (int i = 0; i < 1000; i++) result += array[i % 10] * i;
최적 코드(행렬 곱셈):
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
int r = A[i][k];
for (int j = 0; j < N; j++) {
C[i][j] += r * B[k][j];
}
}
}
프리페처와 그 제한
프리페처는 패턴에 따라 데이터를 미리 로드합니다.
- 순차: for(i) sum += array[i];
- 스트라이드: sum += array[i*2];
제한:
- 랜덤 액세스에서는 작동 안 함.
- 10–20 라인으로 제한.
- 배열 인터리빙에 속음.
마이크로컨트롤러에서는 단순하거나 없음.
대역폭과 멀티코어
DDR4-3200: 채널당 25.6 GB/s, 듀얼 채널 — 51.2 GB/s. L1: ~1000 GB/s.
임베디드: SRAM 1–4 GB/s, DRAM 100–500 MB/s. 작업 세트를 SRAM에 유지하세요.
MESI 프로토콜(x86/ARM):
- M: 수정됨.
- E: 배타적.
- S: 공유.
- I: 무효.
거짓 공유:
struct { int c0, c1; } shared; // Ping-pong
해결:
struct { int c0; char pad[60]; int c1; } shared;
RISC-V: 약한 메모리 모델, fence 사용:
sw a0, 0(a1)
fence w, w
sw a2, 0(a3)
원자성 연산: lr.w/sc.w.
중요한 점
- 캐시 미스가 지배적: 90% 시간이 대기.
- 64 B 라인: 순차 액세스로 15 히트 무료 획득.
- 지역성(공간/시간) — 최적화의 핵심.
- 임베디드 시스템: 단순화된 계층, 프리페처 없음.
- MESI와 패딩으로 멀티코어 핑퐁 방지.
— Editorial Team
아직 댓글이 없습니다.