티스토리 뷰
📅 MemoryProject GC 설계 최종 요약 일지
주제: "Moving GC"의 근본적 딜레마 분석, 모든 해결 방안의 기각, 그리고 "Compaction 포기" 최종 확정
1. 🎯 공통된 핵심 난제: dummy (GC가 모르는 스택 복사본)
모든 대화는 C++ Compaction GC의 가장 근본적인 문제인,
스택에 선언된 **'등록되지 않은 포인터'**가 갱신되지 않는 시나리오에서 시작되었습니다.
문제 시나리오 (모든 대화의 공통 전제):
RootPtr<GameObject> root = CreateObject(); // GC가 알고 있음
ObjectPtr<GameObject> stackCopy = root; // GC가 모름!
// Compaction 발생 시
Compact(); // 객체 이동
// 문제: stackCopy는 여전히 옛날 주소를 가리킴
stackCopy->Method(); // ❌ 댕글링 포인터 크래시!
근본적 원인 (공통된 분석):
C# (Unity)이나 Java와 달리, C++은 런타임에 스택이나 레지스터에 숨어있는 모든 포인터(stackCopy 같은)의 위치를 알려주는 '스택 맵(Stack Map)'이 없습니다.
2. 💡 기각된 해결 방안들 (설계의 진화 과정)
우리는 이 dummy 문제를 100% 안전하게 해결하기 위해 여러 모델을 탐구했지만, 각각 치명적인 결함이 있어 모두 기각했습니다.
A. 기각된 해결책 #1: 명시적 등록 (Chain/Lock) 모델
- 아이디어 (Logs 1, 3):
ObjectPtr의 생성/소멸 시std::mutex를 걸어 전역 리스트(혹은 연결 리스트)에 자신을 등록/해제합니다. - 치명적 결함 (공통된 결론): 성능 재앙.
ObjectPtr p2 = p1;같은 단순 복사 연산이shared_ptr보다 무거운 Lock 연산이 됩니다. 이는 멀티스레드 환경에서 심각한 병목을 유발하며 C++의 '비용 0' 철학을 파괴합니다.
B. 기각된 해결책 #2: Lazy Update (MagicSignature) 모델
- 아이디어 (Logs 1, 3): Compaction 시, 객체의 옛 주소에
MagicSignature와 새 주소를 덮어씁니다.Get()호출 시MagicSignature를 발견하고 스스로 새 주소로 갱신합니다. - 치명적 결함 (Log 1 지적): "GC 2회 실행 시" 버그.
Get()을 한 번도 호출하지 않은dummy가 있는 상태로 GC가 2번 돌면,MagicSignature가 청소(Sweep)됩니다. 그 후dummy->Get()을 호출하면, 재활용된 쓰레기 메모리를 유효한 객체로 착각하여 더 위험한 크래시를 유발합니다.
C. 기각된 해결책 #3: "엄격한 규칙" (Strict Rules) 모델
- 아이디어 (Log 1): Compaction을 수행하되,
dummy크래시는RootPtr를 안 쓴 개발자의 **'규칙 위반'**으로 정의합니다. - 치명적 결함 (Log 2 지적): 안전성 문제.
dummy같은 실수는 개발자가 인지하지 못하는 사이(예: 임시 객체)에도 발생할 수 있습니다. 시스템이 버그를 방치하고 '개발자 탓'으로 돌리는 것은, 디버깅을 불가능하게 만들고 시스템의 안정성을 보장할 수 없습니다.
D. 기각된 해결책 #4: 스택 스캔 (Hybrid) 모델
- 아이디어 (Log 3): GC가 스레드의 스택 전체를 '무식하게' 스캔하여 포인터처럼 보이는 값을 강제로 갱신합니다.
- 치명적 결함 (공통된 분석): 구현 복잡성 및 신뢰성 문제.
- 구현이 극도로 복잡하고 플랫폼에 종속적입니다.
- 정수(int)가 우연히 포인터 주소와 값이 같을 때(False-Positive), 엉뚱한 메모리를 수정하여 더 큰 문제를 일으킬 수 있습니다.
3. 📊 업계 현실 분석
- C++ (Unreal Engine): Compaction 포기
- 이유: 위에서 기각된 (A, B, C, D) 문제들 때문입니다. 100% 안전한 갱신의 비용(성능/복잡성/안전성)이 너무 크므로, Compaction을 포기하고
Binned Allocator/Arena등으로 단편화를 '완화/회피'합니다.
- 이유: 위에서 기각된 (A, B, C, D) 문제들 때문입니다. 100% 안전한 갱신의 비용(성능/복잡성/안전성)이 너무 크므로, Compaction을 포기하고
- C# (Unity Engine): Compaction 수행
- 이유: JIT 런타임이 '스택 맵'을 제공하므로
dummy문제를 100% 안전하게 해결할 수 있습니다.
- 이유: JIT 런타임이 '스택 맵'을 제공하므로
4. 🏁 최종 아키텍처 결정: "Memory Compaction 포기"
우리의 긴 논의는, C++ 환경에서 Compaction을 100% 안전하고 효율적으로 구현하는 것은 사실상 불가능하며, **"Compaction을 하지 않는 것"**이 가장 현명하고 실용적인 설계라는 결론으로 이어졌습니다.
우리의 선택: Unreal Engine (모델 2) 방식 채택
- GC 방식: Mark & Sweep (이동 없음).
- 단편화: Compaction으로 '해결'하는 것을 포기합니다. 대신
Binned Allocator(고정 크기 풀) 또는Arena Allocator(레벨/월드 단위 할당)를 도입하여 단편화를 '완화/회피'합니다. - 핸들 설계:
RootPtr와ObjectPtr의 구분은 Compaction 관점에서는 무의미해집니다. 모든 포인터는 동일하며,RootSet과PROPERTY()는 오직 '도달 가능성(Reachability)'을 **마킹(Marking)**하는 용도로만 사용됩니다. - 결론:
이 탐구 과정은 "Compaction을 왜 안 해야 하는지"를 업계의 현실과 C++의 근본적 한계에 기반하여 논리적으로 완벽하게 방어할 수 있는 강력한 근거가 되었습니다.
'개인 프로젝트 > Memory Project' 카테고리의 다른 글
| [ Memory Project ] 개발 일지 - 25/11/13 (1) | 2025.11.13 |
|---|---|
| [ Memory Project ] 개발 일지 - 25/11/12 (0) | 2025.11.12 |
| Memory 관리 방법 (3) | 2024.01.15 |
- Total
- Today
- Yesterday
- cmake
- 세그먼테이션기법
- RHI
- 외부단편화
- 보이드 포인터
- SFINAE #Template #C++
- std::is_base_of
- GC
- 포인터
- FloatingPoint
- 내부단편화
- 그래픽스
- 수학
- 개발 일지
- logproject
- cmakelists
- Reflection
- void pointer
- 로드리게스 회전 행렬
- 모던 C++ 챌린지
- pointer
- 메모리
- RHICommand
- 뮤택스
- 증명
- C++
- GameEngine
- void* pointer
- MemoryProject
- 페이징기법
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |