운영체제 — 메모리 관리와 가상 메모리
개요 — 메모리 관리가 게임 성능에 미치는 영향
Section titled “개요 — 메모리 관리가 게임 성능에 미치는 영향”게임 개발에서 메모리 관리는 성능의 핵심입니다.
- 오픈 월드 게임에서 스트리밍되는 레벨 에셋은 어떻게 메모리에 로드되는가
- 메모리 단편화가 누적되면 왜 프레임 드롭이 발생하는가
- 32비트 게임이 4GB 이상의 에셋을 사용할 수 없는 이유는 무엇인가
- 페이지 폴트가 게임 스터터링을 유발하는 원인
운영체제의 메모리 관리 메커니즘을 이해하면 이러한 문제를 진단하고 해결하는 능력을 갖추게 됩니다.
1. 물리 메모리와 가상 메모리
Section titled “1. 물리 메모리와 가상 메모리”물리 메모리의 한계
Section titled “물리 메모리의 한계”물리 메모리(RAM)를 직접 사용하는 경우 다음 문제가 발생합니다.
물리 메모리 직접 사용의 문제:1. 메모리 부족: RAM 크기 이상의 프로그램 실행 불가2. 주소 충돌: 여러 프로세스가 같은 주소를 사용하면 충돌3. 보안 취약: 프로세스가 다른 프로세스의 메모리에 접근 가능4. 단편화: 연속된 빈 공간을 찾기 어려워짐가상 메모리 (Virtual Memory)
Section titled “가상 메모리 (Virtual Memory)”가상 메모리는 각 프로세스에게 독립적인 가상 주소 공간을 제공하는 기술입니다.
프로세스 A의 관점: 가상 주소 0x0000 ~ 0xFFFFFFFF (4GB, 32비트 기준) (실제로 RAM에 이만큼 없어도 됨)
운영체제가 관리: 가상 주소 <--> 물리 주소 매핑 자주 쓰는 페이지는 RAM에 유지 덜 쓰는 페이지는 디스크(스왑)로 내보냄가상 메모리의 장점:
- 프로세스마다 독립적인 주소 공간 제공 (메모리 보호)
- 물리 메모리보다 큰 프로그램 실행 가능
- 여러 프로세스 간 메모리 공유 가능 (공유 라이브러리)
- 메모리 관리의 유연성 향상
2. 페이징 (Paging)
Section titled “2. 페이징 (Paging)”페이징은 가상 메모리를 고정 크기의 블록(페이지)으로 나누는 기법입니다.
가상 주소 공간 물리 메모리┌──────────┐ ┌──────────┐│ 페이지 0 │ ──┐ │ 프레임 0 │├──────────┤ │ 페이지 ├──────────┤│ 페이지 1 │ └──테이블──> │ 프레임 3 │├──────────┤ ├──────────┤│ 페이지 2 │ ──────────> │ 프레임 7 │├──────────┤ ├──────────┤│ 페이지 3 │ --> [스왑] │ 프레임 2 │└──────────┘ (디스크) └──────────┘
- 페이지(Page): 가상 주소 공간의 고정 크기 블록 (보통 4KB)- 프레임(Frame): 물리 메모리의 고정 크기 블록 (페이지와 동일 크기)- 페이지 테이블(Page Table): 가상 페이지 -> 물리 프레임 매핑 정보가상 주소 변환 과정
Section titled “가상 주소 변환 과정”가상 주소: [페이지 번호(VPN)] [오프셋] 20비트 12비트 (4KB 페이지 기준)
변환 과정:1. 가상 주소에서 페이지 번호(VPN) 추출2. 페이지 테이블에서 VPN에 해당하는 물리 프레임 번호(PFN) 조회3. PFN + 오프셋 = 물리 주소
예시:가상 주소 0x12345ABC= 페이지 번호: 0x12345 | 오프셋: 0xABC페이지 테이블: 0x12345 -> 물리 프레임 0x00089물리 주소: 0x00089ABC3. TLB (Translation Lookaside Buffer)
Section titled “3. TLB (Translation Lookaside Buffer)”페이지 테이블 조회는 메모리 접근이 필요해 느립니다. TLB는 최근 사용한 주소 변환 결과를 캐싱하는 하드웨어입니다.
CPU가 메모리 접근 시:
1단계: TLB 조회 (1~2 사이클, 매우 빠름) ├─ TLB 히트(Hit): 물리 주소 즉시 반환 └─ TLB 미스(Miss): 페이지 테이블 조회 필요
2단계 (TLB 미스 시): 페이지 테이블 조회 (수십~수백 사이클) ├─ 페이지가 물리 메모리에 있음: 물리 주소 반환, TLB 업데이트 └─ 페이지가 없음(Page Fault): OS 개입 필요
TLB 히트율이 낮아지는 경우:- 매우 큰 데이터셋을 무작위 순서로 접근- 많은 스레드가 서로 다른 메모리 영역 접근-> 게임에서 캐시 친화적 자료구조가 중요한 이유4. 페이지 폴트 (Page Fault)
Section titled “4. 페이지 폴트 (Page Fault)”페이지 폴트는 접근하려는 가상 주소가 현재 물리 메모리에 없을 때 발생하는 인터럽트입니다.
페이지 폴트 처리 과정:
1. CPU가 가상 주소에 접근2. 페이지 테이블 확인 -> 해당 페이지가 메모리에 없음 (Present bit = 0)3. 하드웨어가 Page Fault 인터럽트 발생4. OS 페이지 폴트 핸들러 실행: a. 디스크(스왑 영역)에서 해당 페이지 찾기 b. 물리 메모리에 빈 프레임 확보 (없으면 페이지 교체 알고리즘 실행) c. 디스크에서 물리 프레임으로 페이지 로드 d. 페이지 테이블 업데이트5. 인터럽트된 명령 재실행
소요 시간 비교:- TLB 히트: 1~2ns- 메모리 접근: 50~100ns- 페이지 폴트(SSD): ~0.1ms = 100,000ns- 페이지 폴트(HDD): ~10ms = 10,000,000ns
-> 게임에서 페이지 폴트 = 눈에 띄는 스터터링페이지 교체 알고리즘
Section titled “페이지 교체 알고리즘”물리 메모리가 가득 찼을 때 어떤 페이지를 내보낼지 결정합니다.
| 알고리즘 | 방식 | 특징 |
|---|---|---|
| FIFO | 가장 먼저 들어온 페이지 교체 | 구현 단순, Belady’s Anomaly 발생 가능 |
| LRU | 가장 오래 사용하지 않은 페이지 교체 | 좋은 성능, 구현 비용 높음 |
| LFU | 가장 적게 사용된 페이지 교체 | 최근 자주 쓴 페이지 보호 |
| Clock | LRU 근사 알고리즘 | 실제 OS에서 많이 사용 |
5. 세그멘테이션 (Segmentation)
Section titled “5. 세그멘테이션 (Segmentation)”세그멘테이션은 프로그램을 논리적 단위(코드, 데이터, 스택)로 나누는 기법입니다.
프로세스 메모리 구조:┌──────────────┐ 높은 주소│ 커널 영역 │├──────────────┤│ 스택(Stack)│ <- 지역 변수, 함수 호출 정보│ ↓ │ (높은 주소에서 낮은 주소로 성장)│ ││ ↑ ││ 힙(Heap) │ <- 동적 할당 (new, malloc)├──────────────┤ (낮은 주소에서 높은 주소로 성장)│ BSS 세그먼트 │ <- 초기화되지 않은 전역/정적 변수├──────────────┤│ 데이터 세그먼트│ <- 초기화된 전역/정적 변수├──────────────┤│ 코드 세그먼트 │ <- 실행 코드 (읽기 전용)└──────────────┘ 낮은 주소
// C++ 코드와 메모리 위치 예시:int globalVar = 10; // 데이터 세그먼트int uninitVar; // BSS 세그먼트
void GameLoop(){ int localVar = 5; // 스택 Player* p = new Player(); // 힙 (p 포인터는 스택)
// 함수 종료 시 localVar는 자동 해제, p는 수동 해제 필요 delete p;}6. 메모리 단편화 (Memory Fragmentation)
Section titled “6. 메모리 단편화 (Memory Fragmentation)”외부 단편화 (External Fragmentation)
Section titled “외부 단편화 (External Fragmentation)”메모리 상태 (총 30KB 여유):[사용중 10KB][여유 5KB][사용중 15KB][여유 10KB][사용중 20KB][여유 15KB]
20KB를 연속으로 할당하려고 할 때:-> 실패! 연속된 20KB 공간이 없음 (단편화된 여유 공간만 존재)
해결책:- 페이징: 연속 물리 메모리 불필요, 가상 주소는 연속처럼 보임- 압축(Compaction): 사용 중인 블록을 한쪽으로 모음 (비용 큼)- 메모리 풀(Pool): 미리 큰 블록 할당 후 내부적으로 관리내부 단편화 (Internal Fragmentation)
Section titled “내부 단편화 (Internal Fragmentation)”고정 크기 블록(4KB)으로 할당 시:요청: 100바이트할당: 4096바이트 (4KB 블록 1개)낭비: 3996바이트
-> 페이징에서 발생하는 문제-> 큰 페이지 크기일수록 내부 단편화 증가7. 게임 개발에서의 메모리 관리
Section titled “7. 게임 개발에서의 메모리 관리”메모리 풀 (Memory Pool)
Section titled “메모리 풀 (Memory Pool)”// 게임 오브젝트용 메모리 풀class BulletPool{private: static const int MAX_BULLETS = 1000; Bullet m_Bullets[MAX_BULLETS]; // 연속된 메모리에 미리 할당 bool m_InUse[MAX_BULLETS] = {false};
public: Bullet* Acquire() { for (int i = 0; i < MAX_BULLETS; ++i) { if (!m_InUse[i]) { m_InUse[i] = true; return &m_Bullets[i]; } } return nullptr; // 풀 소진 }
void Release(Bullet* bullet) { int index = bullet - m_Bullets; m_InUse[index] = false; }};
// 장점:// 1. new/delete 없이 O(1) 할당/해제// 2. 메모리 단편화 없음// 3. 캐시 친화적 (연속된 메모리)스택 프레임 할당자 (Stack Allocator)
Section titled “스택 프레임 할당자 (Stack Allocator)”// 프레임 단위 임시 데이터에 적합class FrameAllocator{private: uint8_t* m_Buffer; size_t m_BufferSize; size_t m_CurrentOffset = 0;
public: FrameAllocator(size_t size) : m_BufferSize(size) { m_Buffer = new uint8_t[size]; }
void* Allocate(size_t size) { if (m_CurrentOffset + size > m_BufferSize) return nullptr;
void* ptr = m_Buffer + m_CurrentOffset; m_CurrentOffset += size; return ptr; }
void Reset() // 프레임 끝에 호출 — 모든 임시 할당 해제 { m_CurrentOffset = 0; }};8. 면접 핵심 정리
Section titled “8. 면접 핵심 정리”가상 메모리란? 각 프로세스에게 독립적인 가상 주소 공간을 제공하는 기술입니다. 물리 메모리보다 큰 프로그램 실행을 가능하게 하고, 프로세스 간 메모리 보호를 제공합니다.
페이징과 세그멘테이션의 차이는? 페이징은 고정 크기 단위(페이지)로 메모리를 나누어 외부 단편화를 방지하지만 내부 단편화가 발생합니다. 세그멘테이션은 논리적 단위(코드, 데이터, 스택)로 나누어 의미 있는 메모리 보호가 가능하지만 외부 단편화가 발생합니다.
페이지 폴트가 발생하면? CPU가 인터럽트를 발생시키고 OS가 개입하여 디스크에서 필요한 페이지를 물리 메모리로 로드합니다. 디스크 접근이 필요해 수십~수백 밀리초가 소요될 수 있습니다.
TLB란? 페이지 테이블 조회를 빠르게 하기 위한 하드웨어 캐시입니다. 최근 사용한 가상-물리 주소 변환 결과를 캐싱하여 매번 페이지 테이블을 조회하는 오버헤드를 줄입니다.
- Operating System Concepts (Silberschatz, Galvin, Gagne)
- Linux Kernel Memory Management
- Game Programming Patterns - Object Pool