티스토리 뷰


MemoryProject GC 설계 일지

1. 주제: GC 이동(Compaction) 시 '이동 표식' 전략

GC가 객체를 Pool X에서 Pool Y로 '압축(Compaction)'한 뒤, OldAddress에 남길 '이동 표식'에 대해 두 가지 방식을 비교했습니다.

  • A안 (초기 제안): ForwardingAccessor (C++ 객체)
    • IAccessor를 상속받은 ForwardingAccessorOldAddressplacement new로 생성합니다.
    • BasePtr::Get()이 가상 함수 GetStatus()를 호출하여 eMoved인지 확인합니다.
  • B안 (대안): MagicSignature (단순 데이터)
    • OldAddress0xDEADBEEF 같은 특수 비트 패턴(MAGIC)과 NewAddressmemcpy로 덮어씁니다.
    • BasePtr::Get()이 '가상 함수'가 아닌 '정수 비교' (if (*data == MAGIC))로 eMoved 상태를 확인합니다.

1.1. 제기된 문제점

  • (Turn 8) MagicSignature의 '매직 넘버'가 C++ 객체의 v-table 포인터 주소와 '우연히' 겹칠 수 있지 않은가?
  • (Turn 35, 55) ForwardingAccessormemcpy하면 MagicSignature와 똑같이 안전하지 않은가?
  • (Turn 56) AccessorGetRawPointer()를 호출하는 것도 '가상 함수'인데, GetStatus()와 성능 차이가 있는가?

1.2. 해결 방안 및 결정

  • 위험 식별 (성능): ForwardingAccessor(A안)는 BasePtr::Get()을 호출할 때마다 '가상 함수 호출'을 강제합니다. (Turn 56) 이는 C++ 컴파일러의 '인라이닝'을 막고, CPU의 '분기 예측 실패'를 유발하여, MagicSignature(B안)의 '단순 정수 비교'(비용 0에 수렴)보다 성능이 재앙 수준으로 느립니다. (Turn 48, 57)
  • 위험 식별 (안전성): '표식'을 남길 때(STW 중), ForwardingAccessor~소멸자() / 생성자() / vptr 설정 (C++ 코드 실행)을 요구하며, 이는 '데드락'이나 '데이터 오염'의 위험을 가집니다. (Turn 33, 36) MagicSignaturememcpy (단순 데이터 쓰기)만 하므로 부작용(Side Effect)이 0입니다. (Turn 35)
  • 결정: MagicSignature(B안)가 '런타임 Get() 성능', 'STW 중 안전성', '구현 단순성' 모든 면에서 ForwardingAccessor(A안)보다 압도적으로 우월하므로, MagicSignature 방식을 채택합니다.

2. 주제: GC 실행 모델 및 '프레임 끊김(Hitch)'

'압축(Compaction)' 후 BasePtr들을 '갱신'하는 시점에 대해 논의했습니다.

  • A안 (1-Cycle 강제 갱신): Mark -> Sweep -> Move -> ForceUpdate를 하나의 '거대한 STW(Stop-the-World)'로 처리합니다. GC가 '모든' 살아있는 BasePtr를 '강제 갱신'합니다. MagicSignature는 필요 없습니다.
  • B안 (2-Cycle + Read Barrier): Mark -> Sweep -> Move만 '짧은 STW'로 처리하고, MagicSignature를 남긴 뒤 게임을 '즉시 재개'합니다. BasePtr::Get()if문(Read Barrier)으로 버티다가, 다음 GC(GC 2)가 '강제 갱신'을 마저 처리합니다.

2.1. 제기된 문제점

  • (Turn 46, 57) "Force Update를 하지 않으면, BasePtr::Get()을 호출할 때마다 if문(Read Barrier)을 타야 하는데, 그게 더 무겁지 않을까?"

2.2. 해결 방안 및 결정

  • 분석: "1-Cycle 강제 갱신"(A안)은 STW 중에 '살아있는 모든 객체 그래프'를 순회하며 '쓰기(Write)' 작업을 해야 합니다. 이 '무작위 메모리 접근'은 "캐시 미스 지옥(Cache Miss Hell)"을 유발하여 100ms+의 치명적인 '프레임 끊김(Hitch)'을 발생시킵니다. (Turn 57)
  • 분석: if문(B안)은 99.9%의 시간 동안 False가 되도록 CPU '분기 예측'이 완벽하게 작동하므로, 성능 비용은 0에 수렴합니다. (Turn 48, 57)
  • 결정 (임시): 성능상 "2-Cycle (MagicSignature)"(B안)이 정답이지만, "로직의 명확성"을 위해, 먼저 "1-Cycle 강제 갱신"(A안)을 구현하고, 추후 성능 프로파일링 결과 'Hitch'가 발견되면 "2-Cycle"로 리팩토링하기로 합의했습니다. (Turn 58)

3. 주제: GC 생명 주기 및 "Dangling Pointer (매달린 포인터)" 문제

"1-Cycle 강제 갱신" 모델의 '안전성'에 대해 논의했습니다.

3.1. 제기된 문제점

  • (Turn 18-32, 86, 87) "가장 치명적인 시나리오"가 제기되었습니다:
    1. RootPtr (RootSet에 등록됨)가 Accessor_A를 가리킵니다.
    2. ObjectPtr (복사본)가 RootPtr를 복사했습니다. (RootSet에는 '등록' 안 됨)
    3. CompactionAccessor_APool X -> Pool Y로 이동시킵니다.
    4. ForceUpdateRootSet 스캔하여 RootPtr만 갱신합니다.
    5. ObjectPtr(복사본)는 갱신이 안 되어 Pool X (쓰레기 메모리)를 가리키게 되어 '위험한 동작'이 발생할 수 있습니다.

3.2. 해결 방안 및 결정

  • 전제 수정: ForceUpdateRootSet만 스캔하는 것이 아닙니다. ForceUpdateRootSet에서 '시작'하여, '살아있는 모든 객체 그래프'를 '순회(Traversal)'합니다.
  • 핵심 해결: "살아있는" ObjectPtr(복사본)는 '공중에 뜬' 변수가 아니라, 다른 '살아있는' 객체(예: MySystem)의 '멤버 변수'여야 합니다. (Turn 86, 87)
  • Reflection 시스템 활용: GC는 ForceUpdate 순회 중 MySystem 객체를 '발견(Discover)'합니다. 이때 업로드된 Reflection 프로젝트 API를 사용합니다:
    1. MySystem->GetTypeInfo()를 호출합니다.
    2. GetProperties()로 모든 PropertyInfo 목록을 받습니다.
    3. PropertyInfoObjectPtr 타입인지 확인하고, GetPropertyOffset()을 이용해 해당 ObjectPtr 멤버의 '실제 메모리 주소'를 찾아가 강제로 갱신합니다.
  • 결정: "살아있지만 갱신이 안 된" BasePtr논리적으로 존재할 수 없습니다. '1-Cycle 강제 갱신' 모델은 Reflection 시스템에 '강하게 의존'하여 안전성을 100% 보장합니다.

4. 주제: 메모리 할당 모델 및 "최종 아키텍처" (UE/ECS 비교)

'압축'에 유리한 메모리 레이아웃과 '단편화' 방지 모델(UE 방식) 사이의 '트레이드오프'를 논의했습니다.

4.1. 제기된 문제점

  • (Turn 79) "근본적인 모순 발견": 원하는 [Accessor | T] 연속 블록 모델은 sizeof(Accessor) + sizeof(T)라는 '가변 크기' 할당을 요구합니다. 이것은 UE의 Binned Allocator (Turn 66) 같은 '고정 크기' 풀 모델과 '양립 불가능'합니다.
  • (Turn 75, 76, 81) "API 트레이드오프 발견": '컴파일 방화벽'을 위해 IPoolManager를 '멍청한' char* 할당자(A안)로 만들면, Sweep 단계에서 ~T()를 호출할 방법이 없어 '소멸(Release)'이 불가능해집니다. 반대로 '소멸'을 위해 IPoolManager를 '똑똑한' Allocate<T> 템플릿(B안)으로 만들면, '컴파일 방화벽'이 깨집니다.

4.2. 해결 방안 및 결정

  • 결정 (Binned 폐기): '압축(Compaction)'을 최우선 목표로 하므로, Binned Allocator 모델(UE 방식)을 '폐기'합니다. PoolManager[Accessor | T] '가변 크기' 블록을 다루는 '범용 힙(General-Purpose Heap)'이 되어야 합니다. (Turn 79, 83)
  • 결정 (최종 아키텍처 - "C안"): (Turn 78, 87) UE가 UClass 리플렉션으로 '컴파일 방화벽'과 '런타임 소멸'을 모두 잡는 방식("C안")을 채택합니다. 이 방식은 Reflection 프로젝트를 '열쇠'로 사용합니다.
    1. IAccessor는 템플릿이 아닌 '단일' 클래스가 됩니다.
    2. IAccessorconst Reflection::TypeInfo* m_pTypeInfo;를 멤버로 가집니다.
    3. Memory::MakePtr<T> (API)는 Reflection::GetTypeInfo<T>()를 호출해 TypeInfoPoolManager('구현')에 '데이터'로 전달합니다.
    4. PoolManager는 이 TypeInfo에서 sizeof(T) (및 sizeof(IAccessor))를 읽어 '가변 크기' 블록을 할당하고 '조립'(placement new)합니다.
    5. GC Sweep 시, IAccessor::Release() (가상 함수)가 TypeInfo의 '소멸자 함수 포인터'를 호출해 ~T()를 안전하게 실행합니다. (Turn 78)
    6. GC Compaction 시, PoolManagerIAccessor->GetTypeInfo()를 통해 sizeof(T)를 알아내어 '가변 크기' memmove를 정확히 수행합니다. (Turn 87)

5. 주제: API 접근성 (싱글톤 및 캡슐화)

GarbageCollectorPoolManager 객체에 어떻게 접근할지 논의했습니다.

5.1. 제기된 문제점

  • (Turn 54, 71) RootPtr 생성자가 GC에 접근해야 하는데, GC가 '전역 변수'라면 "초기화 순서 문제(SIOF)"로 RootPtr 생성 시 GCnullptr여서 크래시할 수 있습니다.
  • (Turn 70) namespace에는 private:이 없는데, GC 싱글톤 구현을 어떻게 숨깁니까?
  • (Turn 72, 73) '디버깅'을 위해 GetCollector() API는 노출해야 하지 않습니까?

5.2. 해결 방안 및 결정

  • 결정 (Meyers' Singleton): "초기화 순서 문제(SIOF)"를 100% 안전하게 해결하는 유일한 방법은 "Meyers' Singleton" (함수 내 정적 인스턴스)임을 합의했습니다. (Turn 64, 72)
    • GlobalCollector::Get()이 '최초 호출'되는 그 순간 static instance가 생성되므로, '전역 RootPtr'가 main() 전에 생성되어 Get()을 호출해도 100% 안전합니다.
  • 결정 (Façade 패턴): Memory.h (API 헤더)가 '파사드(정문)' 역할을 합니다. (Turn 68, 70)
    • Public (Memory.h): Memory::GetCollector()Memory::MakePtr<T> API를 '선언'합니다.
    • Private (Memory.cpp): C++의 '익명 네임스페이스(Anonymous Namespace)' (Turn 70)를 사용하여 private:와 동일한 효과를 냅니다.
    • Memory.cppMemory::GetCollector()의 '실제 구현' (Meyers' Singleton)을 숨깁니다. (Turn 72)
    • 결론: RootPtr와 '디버거'는 '공개 API' (Memory::GetCollector())를 안전하게 호출할 수 있고, '구현'은 완벽하게 캡슐화됩니다.
  • 결정 (인터페이스): GetCollector()GlobalCollector& ('구현') 대신 ICollector& ('인터페이스')를 반환하도록 합니다. (Turn 73) 이는 '구현'을 바꾸는 리팩토링이나 '유닛 테스트'를 가능하게 하고, GlobalCollector.h.cpp 내부에 숨겨 "컴파일 방화벽"을 구축함으로써 전체 프로젝트의 컴파일 시간을 극적으로 단축시킵니다. (Turn 73, 74)
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함