컴퓨터 구조

컴퓨터 구조 [2] 메모리 계층 구조 완벽 분석 - 캐시부터 가상 메모리까지

준뜨 2025. 12. 23. 15:35
컴퓨터 구조 [2] 메모리 계층 구조 완벽 분석 - 캐시부터 가상 메모리까지 [2025]
컴퓨터 구조 시리즈 #3

컴퓨터 구조 [2]
메모리 계층 구조 완벽 분석

레지스터부터 디스크까지 - 메모리 시스템의 모든 것

📚 이전 편 복습

[1] CPU 구조편에서 우리는 제어장치, 연산장치, 레지스터의 작동 원리와 명령어 사이클, 파이프라이닝을 배웠습니다. CPU가 명령어를 빠르게 처리하려면 데이터를 빠르게 가져올 수 있어야 합니다. 이번 편에서는 CPU에 데이터를 공급하는 메모리 시스템을 깊이 있게 다룹니다.

📑 목차

  1. 메모리 계층 구조란? - 속도와 용량의 트레이드오프
  2. 4단계 메모리 계층 구조
  3. 캐시 메모리의 동작 원리
  4. 캐시 매핑 기법 (Direct, Associative, Set-Associative)
  5. 가상 메모리와 페이징
  6. 메모리 접근 속도 최적화 기법
  7. 실무 적용: 설비 모니터링 메모리 관리
  8. 다음 편 예고

🏗️ 메모리 계층 구조란? - 속도와 용량의 트레이드오프

메모리 계층 구조(Memory Hierarchy)는 서로 다른 속도와 용량을 가진 메모리들을 계층적으로 배치한 구조입니다.

왜 메모리 계층이 필요한가?

이상적으로는 무한히 빠르고, 무한히 크고, 무한히 저렴한 메모리가 있으면 좋겠지만, 현실에서는 불가능합니다.

💡 메모리의 3가지 제약

  • 속도: 빠른 메모리는 비싸고 용량이 작음
  • 용량: 큰 메모리는 느리고 상대적으로 저렴
  • 가격: 속도와 용량은 반비례 관계

메모리 계층 구조의 핵심 원리

🎯 지역성의 원리 (Principle of Locality)

프로그램은 최근에 사용한 데이터그 근처의 데이터를 다시 사용할 확률이 매우 높습니다.

시간적 지역성 (Temporal Locality)

  • 최근에 접근한 데이터를 가까운 시간 내에 다시 접근
  • 예: 반복문의 카운터 변수, 함수 내의 지역 변수

공간적 지역성 (Spatial Locality)

  • 접근한 데이터의 인근 주소를 곧 접근
  • 예: 배열의 연속된 요소 접근, 순차 코드 실행

메모리 계층 구조의 이점

  • 성능: 자주 쓰는 데이터는 빠른 메모리에 저장
  • 비용: 큰 용량이 필요한 데이터는 저렴한 메모리에 저장
  • 효율성: 전체 시스템의 평균 속도 향상

📊 4단계 메모리 계층 구조

메모리는 CPU에서 가까운 순서대로 4단계 계층으로 구성됩니다.

메모리 피라미드

           빠름 ↑
                │
         ┌──────┴──────┐
         │  레지스터   │ ← 가장 빠름, 가장 작음
         └─────────────┘
        ┌───────────────┐
        │  캐시 메모리   │ ← 매우 빠름, 작음
        └───────────────┘
      ┌───────────────────┐
      │   주기억장치(RAM)  │ ← 빠름, 중간
      └───────────────────┘
    ┌───────────────────────┐
    │ 보조기억장치(SSD/HDD) │ ← 느림, 매우 큼
    └───────────────────────┘
                │
           느림 ↓

각 계층의 상세 비교

1. 레지스터 (Register)
CPU 내부, 32/64비트, 수십 개
0.1 ns
수백 바이트
2. 캐시 (Cache)
L1/L2/L3 3단계, SRAM
1-20 ns
KB ~ MB
3. 주기억장치 (RAM)
DRAM, 휘발성
50-100 ns
GB
4. 보조기억장치 (Storage)
SSD/HDD, 비휘발성
0.1-10 ms
TB ~ PB

속도 차이 실감하기

사람의 시간으로 환산하면:

  • 레지스터 (0.1ns) = 1초
  • 캐시 L1 (1ns) = 10초
  • RAM (100ns) = 16분
  • SSD (0.1ms) = 11일
  • HDD (10ms) = 3년

RAM 접근이 레지스터보다 1000배 느립니다!

메모리 계층 상세표

메모리 접근 시간 용량 가격 (GB당) 휘발성
레지스터 0.1 ns < 1 KB - 휘발성
L1 캐시 1 ns 32-128 KB 매우 고가 휘발성
L2 캐시 3-10 ns 256KB-2MB 고가 휘발성
L3 캐시 10-20 ns 4-32 MB 고가 휘발성
RAM (DRAM) 50-100 ns 4-64 GB $3-10 휘발성
SSD (NVMe) 0.1 ms 256GB-4TB $0.1-0.3 비휘발성
HDD 5-10 ms 500GB-20TB $0.02-0.05 비휘발성

⚡ 캐시 메모리의 동작 원리

캐시 메모리(Cache Memory)는 CPU와 RAM 사이에 위치한 초고속 버퍼 메모리입니다.

캐시의 3단계 구조

┌─────────────────────────────────┐
│             CPU 코어             │
│  ┌──────────┐  ┌──────────┐    │
│  │ L1 캐시  │  │ L1 캐시  │    │ ← 각 코어마다
│  │Data│Inst│  │Data│Inst│    │   독립적으로 존재
│  └──────────┘  └──────────┘    │
│         │            │          │
│  ┌──────┴────────────┴──────┐  │
│  │      L2 캐시 (통합)       │  │ ← 각 코어마다
│  └───────────────────────────┘  │   독립적
└──────────────┬──────────────────┘
               │
        ┌──────┴──────────┐
        │   L3 캐시 (공유) │ ← 모든 코어가 공유
        └──────┬──────────┘
               │
        ┌──────┴──────┐
        │   주기억장치  │
        │   (RAM)     │
        └─────────────┘

캐시 동작 원리

1. 캐시 히트 (Cache Hit)

CPU가 요청한 데이터가 캐시에 있는 경우

CPU: "주소 0x1000의 데이터 주세요"
캐시: "네, 여기 있습니다!" (1ns 소요)
→ 매우 빠름!

2. 캐시 미스 (Cache Miss)

CPU가 요청한 데이터가 캐시에 없는 경우

CPU: "주소 0x2000의 데이터 주세요"
L1 캐시: "없습니다" (1ns)
L2 캐시: "없습니다" (3ns)
L3 캐시: "없습니다" (10ns)
RAM: "여기 있습니다" (100ns)
→ 캐시에 복사 후 CPU에 전달
총 114ns 소요

캐시 성능 지표

캐시 히트율 = (캐시 히트 횟수 ÷ 전체 접근 횟수) × 100%

평균 메모리 접근 시간 (AMAT)

AMAT = 히트 시간 + (미스율 × 미스 페널티)

예시 계산:

  • L1 캐시 히트율: 95%
  • L1 히트 시간: 1ns
  • RAM 접근 시간: 100ns
AMAT = 1ns + (0.05 × 100ns) = 6ns

캐시가 없었다면 평균 100ns → 16배 빠름!

캐시 교체 정책 (Replacement Policy)

캐시가 가득 찼을 때 어떤 데이터를 버릴지 결정하는 알고리즘

정책 설명 장단점
LRU
(Least Recently Used)
가장 오래 사용하지 않은 것을 제거 효과적이지만 구현 복잡
FIFO
(First In First Out)
가장 먼저 들어온 것을 제거 간단하지만 성능 낮음
Random 무작위로 선택해서 제거 간단하고 의외로 효율적
LFU
(Least Frequently Used)
가장 적게 사용된 것을 제거 카운터 필요, 오버헤드

🗺️ 캐시 매핑 기법

캐시 매핑은 메모리 주소를 캐시의 어느 위치에 저장할지 결정하는 방법입니다.

1. 직접 매핑 (Direct Mapping)

메모리 주소를 캐시 블록 번호로 나눈 나머지로 매핑

직접 매핑 구조

메모리 주소: [태그 | 인덱스 | 오프셋]

예: 32비트 주소, 64바이트 블록, 1024개 캐시 라인
- 오프셋: 6비트 (2^6 = 64바이트)
- 인덱스: 10비트 (2^10 = 1024개)
- 태그: 16비트 (나머지)

메모리 블록 0, 1024, 2048, ... → 캐시 라인 0
메모리 블록 1, 1025, 2049, ... → 캐시 라인 1

✅ 장점

  • 하드웨어 구현 간단
  • 검색 속도 빠름 (1번만)
  • 비용 저렴

❌ 단점

  • 캐시 충돌 빈번
  • 히트율 낮음
  • 특정 패턴에 취약

2. 완전 연관 매핑 (Fully Associative Mapping)

메모리 블록을 캐시의 어느 위치에나 저장 가능

메모리 주소: [태그 | 오프셋]

모든 캐시 라인을 병렬로 검색
→ 태그가 일치하는 곳이 있으면 히트

예: 메모리 블록 100 → 캐시의 빈 곳 아무데나

✅ 장점

  • 캐시 충돌 최소화
  • 히트율 최대화
  • 유연한 배치

❌ 단점

  • 하드웨어 복잡/고가
  • 검색 시간 오래 걸림
  • 전력 소모 큼

3. 집합 연관 매핑 (Set-Associative Mapping) ⭐

가장 많이 사용되는 방식으로, 직접 매핑과 완전 연관의 절충안

N-Way Set-Associative (N=4 예시)

메모리 주소: [태그 | 세트 인덱스 | 오프셋]

캐시를 세트로 나누고, 각 세트는 N개의 라인 보유
→ 세트는 인덱스로 결정 (직접 매핑)
→ 세트 내에서는 아무 곳이나 (연관 매핑)

예: 4-way set-associative, 1024개 라인
- 256개 세트, 각 세트당 4개 라인
- 메모리 블록 0 → 세트 0의 4개 라인 중 하나
- 메모리 블록 256 → 세트 0의 4개 라인 중 하나 (경쟁)

💡 실제 CPU 예시

  • Intel Core i7: L1 8-way, L2 4-way, L3 16-way
  • AMD Ryzen: L1 8-way, L2 8-way, L3 16-way

높은 Way 수 = 더 좋은 히트율, 하지만 비용↑

매핑 방식 비교

방식 배치 위치 검색 횟수 히트율 복잡도
직접 매핑 1곳만 가능 1번 낮음 낮음
완전 연관 모든 곳 전체 높음 높음
집합 연관 세트 내 N번 중상 중간

🌐 가상 메모리와 페이징

가상 메모리(Virtual Memory)는 물리 메모리보다 큰 프로그램을 실행할 수 있게 해주는 기술입니다.

가상 메모리의 개념

프로그램은 가상 주소를 사용하고, OS가 이를 물리 주소로 변환합니다.

  • 가상 주소 공간: 각 프로그램이 보는 메모리 (예: 0 ~ 4GB)
  • 물리 주소 공간: 실제 RAM (예: 0 ~ 16GB)
  • 디스크 스왑: RAM 부족 시 디스크 사용

페이징 (Paging)

메모리를 페이지라는 고정 크기 블록으로 나누어 관리

페이징 구조

가상 주소: [페이지 번호 | 오프셋]
물리 주소: [프레임 번호 | 오프셋]

예: 4KB 페이지, 32비트 주소
- 오프셋: 12비트 (2^12 = 4KB)
- 페이지 번호: 20비트 (2^20 = 100만 페이지)

┌─────────────────────────────────┐
│     가상 메모리 (프로그램 뷰)     │
│  ┌─────────┐                    │
│  │ Page 0  │ ─┐                 │
│  ├─────────┤  │                 │
│  │ Page 1  │  │  페이지 테이블   │
│  ├─────────┤  ├─→ [변환]       │
│  │ Page 2  │  │                 │
│  └─────────┘  │                 │
└───────────────┼─────────────────┘
                ↓
┌───────────────┼─────────────────┐
│          물리 메모리 (RAM)       │
│  ┌─────────┐ │                 │
│  │ Frame 5 │←┘ (Page 0)        │
│  ├─────────┤                    │
│  │ Frame 2 │   (Page 1)         │
│  ├─────────┤                    │
│  │ Frame 7 │   (Page 2)         │
│  └─────────┘                    │
└─────────────────────────────────┘

페이지 테이블 (Page Table)

가상 페이지 번호를 물리 프레임 번호로 변환하는 자료구조

가상 페이지 물리 프레임 유효 비트 접근 권한
0 5 1 (RAM에 있음) Read/Write
1 2 1 Read-only
2 - 0 (디스크에 있음) -
3 7 1 Execute

페이지 폴트 (Page Fault)

접근하려는 페이지가 RAM에 없을 때 발생

페이지 폴트 처리 과정

  1. CPU가 가상 주소 접근 시도
  2. 페이지 테이블 확인 → 유효 비트 = 0
  3. 페이지 폴트 발생! (예외 처리)
  4. OS가 디스크에서 해당 페이지를 읽어옴 (수 밀리초)
  5. RAM에 빈 프레임이 없으면 → 기존 페이지 내보냄 (스왑)
  6. 새 페이지를 RAM에 적재
  7. 페이지 테이블 업데이트
  8. 명령어 재실행

⚠️ 페이지 폴트는 매우 느립니다! (10,000배 이상)

페이지 교체 알고리즘

알고리즘 설명 특징
FIFO 가장 먼저 들어온 페이지 교체 간단하지만 비효율적
LRU 가장 오래 사용하지 않은 페이지 교체 효과적, 구현 복잡
Clock 참조 비트 사용, 순환 검색 LRU 근사, 효율적
LFU 가장 적게 사용된 페이지 교체 카운터 필요

TLB (Translation Lookaside Buffer)

TLB는 페이지 테이블의 캐시입니다. 최근 사용한 주소 변환 정보를 저장합니다.

  • TLB 히트: 주소 변환 즉시 완료 (1클럭)
  • TLB 미스: 페이지 테이블 접근 필요 (수십 클럭)
  • 히트율: 보통 95~99%

🚀 메모리 접근 속도 최적화 기법

1. 캐시 친화적 코드 작성

✅ 좋은 예: 행 우선 접근

// 2차원 배열 순회 - 캐시 친화적
int data[1000][1000];

// 행 우선 (Row-major): 메모리 순서대로 접근
for (int i = 0; i < 1000; i++)
  for (int j = 0; j < 1000; j++)
    sum += data[i][j];  // 캐시 히트율 높음

// Delphi 버전
for i := 0 to 999 do
  for j := 0 to 999 do
    Sum := Sum + Data[i, j];

❌ 나쁜 예: 열 우선 접근

// 열 우선 (Column-major): 메모리 점프
for (int j = 0; j < 1000; j++)
  for (int i = 0; i < 1000; i++)
    sum += data[i][j];  // 캐시 미스 폭증!

// 성능 차이: 3~10배 느림

2. 데이터 구조 최적화

❌ Structure of Arrays (비효율)

type
  TParticles = record
    X: array[0..9999] of Single;
    Y: array[0..9999] of Single;
    Z: array[0..9999] of Single;
  end;

// 업데이트 시 캐시 미스

✅ Array of Structures (효율)

type
  TParticle = record
    X, Y, Z: Single;
  end;
  TParticles = array[0..9999]
    of TParticle;

// 업데이트 시 캐시 히트

3. 프리페칭 (Prefetching)

미리 필요한 데이터를 캐시로 가져오는 기법

하드웨어 프리페칭

  • CPU가 접근 패턴을 자동 감지
  • 순차 접근 시 다음 데이터를 미리 로드
  • 대부분의 현대 CPU가 지원

소프트웨어 프리페칭

// C/C++ 예시
for (int i = 0; i < n; i++) {
  __builtin_prefetch(&data[i+8]);  // 미리 로드
  process(data[i]);
}

4. 메모리 정렬 (Memory Alignment)

데이터를 캐시 라인 경계에 맞춰 배치

  • 캐시 라인 크기: 보통 64바이트
  • 정렬: 데이터 시작 주소를 64바이트 배수로
  • 패딩: 빈 공간 추가로 정렬 맞춤
// Delphi 정렬 예시
type
  {$ALIGN 64}  // 64바이트 정렬
  TCacheAlignedData = record
    Value1: Integer;
    Value2: Integer;
    // 나머지 56바이트는 패딩
  end;

5. 페이지 폴트 최소화

  • 워킹 셋 관리: 자주 쓰는 데이터를 RAM에 유지
  • 메모리 풀: 미리 할당해서 재사용
  • 대용량 페이지: Huge Pages 사용 (2MB/1GB)
  • 메모리 락: 중요한 데이터는 RAM 고정

💼 실무 적용: 설비 모니터링 메모리 관리

사례 1: 링 버퍼로 캐시 효율 극대화

문제: 1초마다 16채널 센서 데이터를 저장하면서 최근 1시간 데이터 조회

// 나쁜 예: 동적 할당 (페이지 폴트 유발)
var
  LogList: TList;
begin
  // 매번 새로운 메모리 할당
  LogList.Add(NewSensorData);  // 페이지 폴트 가능
end;

// 좋은 예: 링 버퍼 (캐시 친화적)
const
  BUFFER_SIZE = 3600 * 16;  // 1시간 × 16채널
var
  RingBuffer: array[0..BUFFER_SIZE-1] of TSensorData;
  WriteIndex: Integer = 0;

procedure AddData(const Data: TSensorData);
begin
  RingBuffer[WriteIndex] := Data;
  WriteIndex := (WriteIndex + 1) mod BUFFER_SIZE;
  // 고정된 메모리 영역 재사용 → 캐시 효율 극대화
end;

사례 2: 메모리 풀 활용

시나리오: 알람 이벤트 객체를 빈번하게 생성/소멸

// 메모리 풀 구현
type
  TAlarmPool = class
  private
    FPool: TStack;
  public
    function Acquire: TAlarmEvent;
    procedure Release(Event: TAlarmEvent);
    constructor Create(PoolSize: Integer);
  end;

constructor TAlarmPool.Create(PoolSize: Integer);
var
  i: Integer;
begin
  FPool := TStack.Create;
  // 미리 할당
  for i := 0 to PoolSize - 1 do
    FPool.Push(TAlarmEvent.Create);
end;

function TAlarmPool.Acquire: TAlarmEvent;
begin
  if FPool.Count > 0 then
    Result := FPool.Pop  // 재사용
  else
    Result := TAlarmEvent.Create;  // 부족하면 새로 생성
end;

procedure TAlarmPool.Release(Event: TAlarmEvent);
begin
  Event.Clear;
  FPool.Push(Event);  // 풀에 반환
end;

// 사용 예
var
  Alarm: TAlarmEvent;
begin
  Alarm := AlarmPool.Acquire;
  try
    // 사용
  finally
    AlarmPool.Release(Alarm);
  end;
end;

// 효과: 메모리 할당/해제 99% 감소!

사례 3: 대용량 로그 파일 처리

문제: 100MB 로그 파일을 분석해야 하는데 RAM은 제한적

// 나쁜 예: 전체 파일 로드
var
  AllData: TStringList;
begin
  AllData := TStringList.Create;
  AllData.LoadFromFile('huge.log');  // 100MB RAM 사용!
  // 처리...
end;

// 좋은 예: 스트리밍 방식
var
  FileStream: TFileStream;
  Buffer: array[0..8191] of Byte;  // 8KB 버퍼
  BytesRead: Integer;
begin
  FileStream := TFileStream.Create('huge.log', fmOpenRead);
  try
    repeat
      BytesRead := FileStream.Read(Buffer, SizeOf(Buffer));
      ProcessChunk(Buffer, BytesRead);
      // 8KB씩만 메모리 사용
    until BytesRead = 0;
  finally
    FileStream.Free;
  end;
end;

// 메모리 사용: 100MB → 8KB (99.99% 절감!)

사례 4: 캐시 라인 충돌 회피

문제: 멀티스레드에서 카운터 변수 경합

// 나쁜 예: False Sharing (거짓 공유)
type
  TCounters = record
    Counter1: Integer;  // 0바이트
    Counter2: Integer;  // 4바이트
    Counter3: Integer;  // 8바이트
    Counter4: Integer;  // 12바이트
  end;
// 모두 같은 캐시 라인(64바이트)에 존재
// 스레드1이 Counter1 수정 → 캐시 라인 무효화
// 스레드2의 Counter2도 영향받음!

// 좋은 예: 캐시 라인 분리
type
  {$ALIGN 64}
  TPaddedCounter = record
    Value: Integer;
    Padding: array[0..59] of Byte;  // 60바이트 패딩
  end;

  TCounters = record
    Counter1: TPaddedCounter;  // 0~63 바이트
    Counter2: TPaddedCounter;  // 64~127 바이트
    Counter3: TPaddedCounter;  // 128~191 바이트
    Counter4: TPaddedCounter;  // 192~255 바이트
  end;
// 각 카운터가 독립된 캐시 라인에 위치
// 스레드 간 간섭 없음

// 성능 향상: 4~8배!

실무 최적화 체크리스트

  • ✅ 순차 접근 패턴 사용 (배열 순회 시 행 우선)
  • ✅ 자주 쓰는 데이터는 지역 변수로
  • ✅ 큰 구조체는 포인터로 전달
  • ✅ 메모리 풀로 할당 오버헤드 제거
  • ✅ 링 버퍼로 고정 메모리 재사용
  • ✅ 멀티스레드는 캐시 라인 분리
  • ✅ 대용량 파일은 스트리밍 처리
  • ✅ 프로파일러로 캐시 미스율 측정

🚀 다음 편 예고: 컴퓨터 구조 [3] - 입출력 시스템

다음 포스팅에서는 입출력(I/O) 시스템을 깊이 있게 다룰 예정입니다.

다룰 내용:

  • 입출력 방식 (Polling, Interrupt, DMA)
  • 버스 시스템 구조와 종류
  • 디스크 I/O 최적화 기법
  • 통신 프로토콜 (UART, SPI, I2C, Modbus)
  • 설비 모니터링에서의 실시간 I/O 처리 전략

📌 핵심 요약

  • 메모리는 속도와 용량의 트레이드오프로 계층 구조 형성
  • 캐시 메모리는 지역성 원리로 CPU 성능 극대화 (히트율 95% 목표)
  • 집합 연관 매핑이 가장 효율적인 캐시 매핑 방식
  • 가상 메모리는 페이징으로 물리 메모리보다 큰 프로그램 실행 가능
  • 페이지 폴트는 매우 느리므로 최소화가 중요 (10,000배 차이)
  • 실무에서는 캐시 친화적 코드, 메모리 풀, 링 버퍼 활용 필수

🏷️ 태그

#메모리계층구조 #캐시메모리 #가상메모리 #페이징 #캐시매핑 #메모리최적화 #RAM #컴퓨터구조 #성능튜닝 #설비모니터링

💬 여러분의 경험을 공유해주세요!

메모리 최적화로 성능을 크게 개선한 경험이 있으신가요?
또는 캐시, 가상 메모리에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!

728x90
SMALL