티스토리 뷰


📅 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가 스레드의 스택 전체를 '무식하게' 스캔하여 포인터처럼 보이는 값을 강제로 갱신합니다.
  • 치명적 결함 (공통된 분석): 구현 복잡성 및 신뢰성 문제.
    1. 구현이 극도로 복잡하고 플랫폼에 종속적입니다.
    2. 정수(int)가 우연히 포인터 주소와 값이 같을 때(False-Positive), 엉뚱한 메모리를 수정하여 더 큰 문제를 일으킬 수 있습니다.

3. 📊 업계 현실 분석

  • C++ (Unreal Engine): Compaction 포기
    • 이유: 위에서 기각된 (A, B, C, D) 문제들 때문입니다. 100% 안전한 갱신의 비용(성능/복잡성/안전성)이 너무 크므로, Compaction을 포기하고 Binned Allocator / Arena 등으로 단편화를 '완화/회피'합니다.
  • C# (Unity Engine): Compaction 수행
    • 이유: JIT 런타임이 '스택 맵'을 제공하므로 dummy 문제를 100% 안전하게 해결할 수 있습니다.

4. 🏁 최종 아키텍처 결정: "Memory Compaction 포기"

우리의 긴 논의는, C++ 환경에서 Compaction을 100% 안전하고 효율적으로 구현하는 것은 사실상 불가능하며, **"Compaction을 하지 않는 것"**이 가장 현명하고 실용적인 설계라는 결론으로 이어졌습니다.

우리의 선택: Unreal Engine (모델 2) 방식 채택

  • GC 방식: Mark & Sweep (이동 없음).
  • 단편화: Compaction으로 '해결'하는 것을 포기합니다. 대신 Binned Allocator (고정 크기 풀) 또는 Arena Allocator (레벨/월드 단위 할당)를 도입하여 단편화를 '완화/회피'합니다.
  • 핸들 설계: RootPtrObjectPtr의 구분은 Compaction 관점에서는 무의미해집니다. 모든 포인터는 동일하며, RootSetPROPERTY()는 오직 '도달 가능성(Reachability)'을 **마킹(Marking)**하는 용도로만 사용됩니다.
  • 결론:
    이 탐구 과정은 "Compaction을 왜 안 해야 하는지"를 업계의 현실과 C++의 근본적 한계에 기반하여 논리적으로 완벽하게 방어할 수 있는 강력한 근거가 되었습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
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
글 보관함