멀티스레드 GC
Parallel GC, Worker Thread 활용, UE 5.3+ 개선사항으로 GC를 병렬화하는 최신 기법
GC의 스레딩 모델
UE5 GC가 멀티코어를 활용하는 방식
GC 단계별 스레딩
| 단계 | 스레딩 | 설명 |
|---|---|---|
| GC Lock 획득 | Game Thread | 다른 스레드의 UObject 조작 차단 |
| Unreachable 초기화 | Parallel | Worker Thread로 병렬 플래그 설정 |
| Root Set 수집 | Game Thread | FGCObject 목록 순회 |
| 참조 순회 (Mark) | Parallel | TFastReferenceCollector 병렬 실행 |
| Unhash Unreachable | Game Thread | 해시 테이블 정리 |
| BeginDestroy | Game Thread | 소멸 시작 (순차) |
| FinishDestroy | Async 가능 | 비동기 완료 대기 |
GC가 진행 중이면 FGCScopeGuard를 통해 다른 스레드의 UObject 생성/파괴가 차단됩니다. 이는 게임 스레드를 포함한 모든 스레드에 영향을 미칩니다. 따라서 GC 시간을 최소화하는 것이 전체 프레임 성능에 중요합니다.
Parallel Mark 구현
참조 순회를 병렬로 처리하는 메커니즘
병렬 참조 수집기
// 병렬 Mark 단계
void PerformReachabilityAnalysisOnObjects(FGCArrayStruct& ArrayStruct)
{
// 작업을 Worker Thread에 분배
int32 NumWorkers = FTaskGraphInterface::Get()
.GetNumWorkerThreads();
// 객체 목록을 Worker 수에 맞게 분할
int32 ObjectsPerWorker = ArrayStruct.Num() / NumWorkers;
ParallelFor(NumWorkers, [&](int32 WorkerIndex)
{
int32 Start = WorkerIndex * ObjectsPerWorker;
int32 End = (WorkerIndex == NumWorkers - 1)
? ArrayStruct.Num() : Start + ObjectsPerWorker;
// 각 Worker가 할당된 범위의 객체 참조를 수집
TFastReferenceCollector<FGCReferenceProcessor> Collector;
Collector.ProcessObjectArray(ArrayStruct, Start, End);
});
// 결과 병합 (Atomic 연산 사용)
}
병렬 Mark에서 여러 Worker가 같은 객체를 Reachable로 마킹할 수 있습니다. 이를 위해 EInternalObjectFlags의 설정/해제는 atomic 연산을 사용합니다. 또한 Worker 간 작업 큐는 lock-free 구조를 사용하여 경합을 최소화합니다.
UE 5.3+ GC 개선사항
최신 엔진 버전의 GC 성능 개선
버전별 GC 개선
| 버전 | 개선사항 | 효과 |
|---|---|---|
| UE 5.0 | TObjectPtr 도입, 쓰기 배리어 | Incremental GC 안전성 향상 |
| UE 5.1 | Cluster GC 개선 | 대규모 에셋의 GC 효율 향상 |
| UE 5.2 | Incremental Mark 최적화 | Mark 단계 분산 효율 개선 |
| UE 5.3 | Parallel Reachability Analysis | 멀티코어 활용도 대폭 향상 |
| UE 5.4 | Incremental Unhash 도입 | Unhash 단계도 점진적 처리 |
| UE 5.5+ | GC 스케줄러 개선 | 적응형 GC 타이밍 |
// UE 5.3+ 병렬 Reachability 활용
// DefaultEngine.ini
gc.AllowParallelReachability=True // 5.3+에서 기본 활성화
gc.ParallelGCBatchSize=4096 // Worker당 처리 배치 크기
// 5.5+ 적응형 GC 스케줄링
// 프레임 여유 시간에 맞춰 자동으로 GC 작업량 조절
gc.AdaptiveGCBudget=True
멀티스레드 GC 주의사항
병렬 GC 환경에서의 안전한 프로그래밍
스레드 안전 가이드
// 1. 다른 스레드에서 UObject 접근 시
// GC Lock을 획득해야 함
{
FGCScopeGuard GCGuard;
// 이 스코프 내에서는 GC가 실행되지 않음
UObject* Obj = SomeWeakPtr.Get();
if (Obj)
{
// 안전하게 사용
}
}
// 2. 비동기 태스크에서 UObject 참조
TWeakObjectPtr<UMyObject> WeakObj = MyObj;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,
[WeakObj]()
{
// 반드시 Game Thread에서 UObject 접근
AsyncTask(ENamedThreads::GameThread,
[WeakObj]()
{
if (UMyObject* Obj = WeakObj.Get())
{
Obj->ProcessResult();
}
});
});
- 백그라운드 스레드에서 NewObject / SpawnActor 호출
- GC Lock 없이 다른 스레드에서 UObject 프로퍼티 수정
- 백그라운드에서 raw UObject* 캐싱 (TWeakObjectPtr 사용)
- FGCScopeGuard를 장시간 유지 (GC 차단 → 메모리 증가)
핵심 요약
- UE5 GC의 Mark 단계는 Worker Thread에서 병렬로 실행됩니다
- GC Lock은 GC 진행 중 모든 스레드의 UObject 조작을 차단합니다
- UE 5.3+에서 Parallel Reachability Analysis로 멀티코어 활용이 크게 향상되었습니다
- 다른 스레드에서 UObject 접근 시 FGCScopeGuard로 보호해야 합니다
- 비동기 태스크에서는 TWeakObjectPtr + Game Thread 콜백 패턴을 사용합니다
도전 과제
배운 내용을 직접 실습해보세요
gc.AllowParallelGC와 gc.NumReachabilityWorkersToUse의 현재 설정값을 확인하고, 값을 변경하면서 GC Mark 단계 소요 시간의 변화를 측정하세요. CPU 코어 수에 따른 최적 워커 수를 실험적으로 도출하세요.
멀티스레드 환경에서 GC가 실행되는 동안 다른 스레드에서 UObject 접근 시 발생할 수 있는 문제를 재현하세요. FGCScopeGuard, IsInGameThread(), IsGarbageCollecting() 등의 API를 사용한 안전한 코드 패턴을 구현하세요.
Unreal Insights의 CPU 타이밍 뷰를 사용하여 병렬 GC의 스레드별 작업 분포를 분석하세요. Reachability Analysis Worker들의 작업량 밸런스를 확인하고, 불균형이 있다면 원인을 분석하여 개선 방안을 제시하세요.