티스토리 뷰
MemoryProject GC 설계 일지
1. 주제: GC 이동(Compaction) 시 '이동 표식' 전략
GC가 객체를 Pool X에서 Pool Y로 '압축(Compaction)'한 뒤, OldAddress에 남길 '이동 표식'에 대해 두 가지 방식을 비교했습니다.
- A안 (초기 제안):
ForwardingAccessor(C++ 객체)IAccessor를 상속받은ForwardingAccessor를OldAddress에placement new로 생성합니다.BasePtr::Get()이 가상 함수GetStatus()를 호출하여eMoved인지 확인합니다.
- B안 (대안):
MagicSignature(단순 데이터)OldAddress에0xDEADBEEF같은 특수 비트 패턴(MAGIC)과NewAddress를memcpy로 덮어씁니다.BasePtr::Get()이 '가상 함수'가 아닌 '정수 비교' (if (*data == MAGIC))로eMoved상태를 확인합니다.
1.1. 제기된 문제점
- (Turn 8)
MagicSignature의 '매직 넘버'가 C++ 객체의v-table포인터 주소와 '우연히' 겹칠 수 있지 않은가? - (Turn 35, 55)
ForwardingAccessor를memcpy하면MagicSignature와 똑같이 안전하지 않은가? - (Turn 56)
Accessor가GetRawPointer()를 호출하는 것도 '가상 함수'인데,GetStatus()와 성능 차이가 있는가?
1.2. 해결 방안 및 결정
- 위험 식별 (성능):
ForwardingAccessor(A안)는BasePtr::Get()을 호출할 때마다 '가상 함수 호출'을 강제합니다. (Turn 56) 이는 C++ 컴파일러의 '인라이닝'을 막고, CPU의 '분기 예측 실패'를 유발하여,MagicSignature(B안)의 '단순 정수 비교'(비용 0에 수렴)보다 성능이 재앙 수준으로 느립니다. (Turn 48, 57) - 위험 식별 (안전성): '표식'을 남길 때(STW 중),
ForwardingAccessor는~소멸자()/생성자()/vptr 설정(C++ 코드 실행)을 요구하며, 이는 '데드락'이나 '데이터 오염'의 위험을 가집니다. (Turn 33, 36)MagicSignature는memcpy(단순 데이터 쓰기)만 하므로 부작용(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) "가장 치명적인 시나리오"가 제기되었습니다:
RootPtr(RootSet에 등록됨)가Accessor_A를 가리킵니다.ObjectPtr(복사본)가RootPtr를 복사했습니다. (RootSet에는 '등록' 안 됨)Compaction이Accessor_A를Pool X->Pool Y로 이동시킵니다.ForceUpdate가RootSet만 스캔하여RootPtr만 갱신합니다.ObjectPtr(복사본)는 갱신이 안 되어Pool X(쓰레기 메모리)를 가리키게 되어 '위험한 동작'이 발생할 수 있습니다.
3.2. 해결 방안 및 결정
- 전제 수정:
ForceUpdate는RootSet만 스캔하는 것이 아닙니다.ForceUpdate는RootSet에서 '시작'하여, '살아있는 모든 객체 그래프'를 '순회(Traversal)'합니다. - 핵심 해결: "살아있는"
ObjectPtr(복사본)는 '공중에 뜬' 변수가 아니라, 다른 '살아있는' 객체(예:MySystem)의 '멤버 변수'여야 합니다. (Turn 86, 87) Reflection시스템 활용: GC는ForceUpdate순회 중MySystem객체를 '발견(Discover)'합니다. 이때 업로드된Reflection프로젝트 API를 사용합니다:MySystem->GetTypeInfo()를 호출합니다.GetProperties()로 모든PropertyInfo목록을 받습니다.PropertyInfo가ObjectPtr타입인지 확인하고,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프로젝트를 '열쇠'로 사용합니다.IAccessor는 템플릿이 아닌 '단일' 클래스가 됩니다.IAccessor는const Reflection::TypeInfo* m_pTypeInfo;를 멤버로 가집니다.Memory::MakePtr<T>(API)는Reflection::GetTypeInfo<T>()를 호출해TypeInfo를PoolManager('구현')에 '데이터'로 전달합니다.PoolManager는 이TypeInfo에서sizeof(T)(및sizeof(IAccessor))를 읽어 '가변 크기' 블록을 할당하고 '조립'(placement new)합니다.GC Sweep시,IAccessor::Release()(가상 함수)가TypeInfo의 '소멸자 함수 포인터'를 호출해~T()를 안전하게 실행합니다. (Turn 78)GC Compaction시,PoolManager가IAccessor->GetTypeInfo()를 통해sizeof(T)를 알아내어 '가변 크기'memmove를 정확히 수행합니다. (Turn 87)
5. 주제: API 접근성 (싱글톤 및 캡슐화)
GarbageCollector와 PoolManager 객체에 어떻게 접근할지 논의했습니다.
5.1. 제기된 문제점
- (Turn 54, 71)
RootPtr생성자가GC에 접근해야 하는데,GC가 '전역 변수'라면 "초기화 순서 문제(SIOF)"로RootPtr생성 시GC가nullptr여서 크래시할 수 있습니다. - (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.cpp에Memory::GetCollector()의 '실제 구현' (Meyers' Singleton)을 숨깁니다. (Turn 72)- 결론:
RootPtr와 '디버거'는 '공개 API' (Memory::GetCollector())를 안전하게 호출할 수 있고, '구현'은 완벽하게 캡슐화됩니다.
- Public (
- 결정 (인터페이스):
GetCollector()가GlobalCollector&('구현') 대신ICollector&('인터페이스')를 반환하도록 합니다. (Turn 73) 이는 '구현'을 바꾸는 리팩토링이나 '유닛 테스트'를 가능하게 하고,GlobalCollector.h를.cpp내부에 숨겨 "컴파일 방화벽"을 구축함으로써 전체 프로젝트의 컴파일 시간을 극적으로 단축시킵니다. (Turn 73, 74)
'개인 프로젝트 > Memory Project' 카테고리의 다른 글
| [ Memory Project ] 개발 일지 - 25/11/17 (0) | 2025.11.17 |
|---|---|
| [ Memory Project ] 개발 일지 - 25/11/13 (1) | 2025.11.13 |
| Memory 관리 방법 (3) | 2024.01.15 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 모던 C++ 챌린지
- GC
- SFINAE #Template #C++
- 수학
- 페이징기법
- 외부단편화
- C++
- 포인터
- cmakelists
- pointer
- MemoryProject
- 증명
- void* pointer
- 메모리
- void pointer
- 그래픽스
- RHI
- logproject
- EulerRotation
- Reflection
- EulerAngle
- 짐볼락
- 로드리게스 회전 행렬
- cmake
- std::is_base_of
- 내부단편화
- 개발 일지
- 세그먼테이션기법
- RHICommand
- 보이드 포인터
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
글 보관함
