티스토리 뷰


MemoryProject GC 설계 일지

주제: 메모리 할당 모델 변경, 컴파일 방화벽 트레이드오프, 그리고 API 구조 최적화


1. 메모리 할당 모델: "Binned" 폐기 및 "Bump Allocator" 채택

객체와 Accessor의 메모리 배치 방식과 그에 따른 할당 전략을 재검토했습니다.

  • 기존 계획: UE 스타일의 Binned Allocator (고정 크기 풀) 사용.
  • 제기된 문제점:
    • "근본적 모순": 우리가 선택한 [Accessor | T] 연속 블록 모델은 T마다 크기가 다른 '가변 크기(Variable Size)' 할당을 요구합니다. 이는 고정 크기 풀(Pool_32, Pool_64...)과는 구조적으로 호환되지 않습니다.
  • 논의된 대안:
    • Next-Fit (순환 탐색): 빈 공간을 찾아 리스트를 순회함. -> 할당 속도가 느림(O(N)).
    • Free List: 빈 공간 목록 관리. -> 단편화 및 관리 오버헤드.
  • 최종 결정: Bump Allocator (순차 할당)
    • 전략: 평소에는 ptr += sizeO(1)의 초고속 할당을 수행하고, 빈 공간(구멍)은 무시합니다.
    • 해결: 메모리가 꽉 차면 GC(Compaction)를 트리거하여 구멍을 없애고 ptr를 초기화합니다. 이것이 현대 GC의 표준 모델임을 확인했습니다.

2. 컴파일 방화벽 vs 런타임 동작 (The Great Trade-off)

Memory.h의 헤더 의존성을 줄이면서(컴파일 속도), 템플릿 객체 T를 올바르게 생성/소멸시키는 딜레마를 해결했습니다.

  • 딜레마 상황:
    • IPoolManager를 인터페이스로 만들면 Allocate<T> 템플릿을 가질 수 없습니다. (가상 함수는 템플릿 불가)
    • IPoolManagervoid*만 다루게 하면(Type Erasure), Sweep 단계에서 ~T() 소멸자를 호출할 방법이 없습니다.
  • 해결 시도 (C안):
    • UE처럼 TypeInfo에 소멸자 함수 포인터를 담아 전달하는 방식. -> Reflection 모듈 수정이 필요하여 기각.
  • 최종 결정: "책임 분리 (B안)"
    • Memory.h (API): MakePtr<T> 템플릿 구현을 포함하여 헤더가 무거워지는 것을 감수합니다. MakePtrT의 생성(placement new)과 예외 처리(try-catch)를 전담합니다.
    • IPoolManager (구현): AllocateBlock(size)만 수행하는 '멍청한' 인터페이스로 유지하여 컴파일 방화벽을 지킵니다.
    • Accessor<T> (템플릿): virtual Release() 함수가 ~T() 소멸자 호출을 책임져 런타임 안전성을 확보합니다.

3. API 설계 및 순환 참조(Circular Dependency) 해결

RootPtr, MemoryCore, Memory 간의 복잡한 포함 관계를 정리했습니다.

  • 제기된 문제점:
    • "Memory.hRootPtr.h를 포함하고, RootPtr 생성자가 Memory::GetCollector()를 호출하기 위해 다시 Memory.h를 필요로 하는 순환 참조가 발생한다."
    • "전역 RootPtr 생성 시 GC 싱글톤이 초기화되지 않아 크래시가 발생할 수 있다 (SIOF)."
  • 최종 결정: 계층형 헤더 구조 (Layered Architecture)
    1. MemoryCore.h (Layer 0): 핵심 API(MakePtr, GetCollector 선언) 및 기본 타입. RootPtr를 모름.
    2. RootPtr.h (Layer 1): MemoryCore.h를 포함하여 GC에 접근.
    3. Memory.h (Layer 2): 사용자를 위한 마스터 헤더. 위 두 헤더를 모두 포함.
  • 안전성 확보: MemoryCore.cpp 내부에서 Meyers' Singleton (static instance)을 사용하여 초기화 순서 문제를 원천 차단했습니다.

4. Pool 구현 상세 및 Accessor 활용

  • 제기된 문제점:
    • "Pool은 Accessor를 모르는 게(캡슐화) 좋지 않나?"
    • "Pool이 메모리(Sweep)를 순회하려면 각 블록의 크기를 알아야 하는데, Accessor를 모르면 BlockHeader를 또 붙여야 해서 메모리가 낭비된다."
  • 최종 결정: Accessor as Header
    • 캡슐화를 위해 BlockHeader를 추가하는 것은 메모리 낭비입니다.
    • Pool 구현부(Pool.cpp)에서 IAccessor.h를 포함하도록 하여, Accessor 자체를 메모리 블록의 헤더로 활용합니다.
    • Pool은 AccessorTypeInfo를 통해 블록 크기를 알아내고 CompactionSweep을 수행합니다.

5. 배열 지원 API

  • 논의: MakePtr[4] 같은 문법이 가능한가?
  • 결정: 비표준 문법 대신 C++ 표준(std::make_shared)과 동일한 MakePtr<T[]>(count) 방식을 채택했습니다. 이를 위해 Accessor<T[]> 부분 특수화를 구현하여 배열 개수 관리 및 소멸자 역순 호출을 지원하기로 했습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함