메모리 관리 기초
언리얼 엔진에서의 효과적인 메모리 관리는 게임의 성능과 안정성을 위해 중요합니다.
이 절에서는 C++ 프로그래밍 관점에서 언리얼 엔진의 메모리 관리 기법과 주의사항을 살펴보겠습니다.
언리얼 엔진의 메모리 할당자 시스템
언리얼 엔진은 자체적인 메모리 할당자 시스템을 제공합니다.
이 시스템은 다음과 같은 특징을 가집니다.
- 메모리 풀링
- 멀티스레드 지원
- 메모리 단편화 최소화
기본 메모리 할당자 사용 예
void* Memory = FMemory::Malloc(Size);
FMemory::Free(Memory);
메모리 풀
메모리 풀은 자주 사용되는 객체의 생성과 소멸 비용을 줄이는 데 효과적입니다.
class FMyObjectPool
{
public:
TSharedPtr<FMyObject> Allocate()
{
if (Pool.Num() > 0)
{
return Pool.Pop();
}
return MakeShared<FMyObject>();
}
void Deallocate(TSharedPtr<FMyObject> Object)
{
Pool.Push(Object);
}
private:
TArray<TSharedPtr<FMyObject>> Pool;
};
커스텀 할당자 구현
특정 용도에 최적화된 커스텀 할당자를 구현할 수 있습니다.
class FMyCustomAllocator : public FMalloc
{
public:
virtual void* Malloc(SIZE_T Size, uint32 Alignment) override
{
// 커스텀 할당 로직
}
virtual void Free(void* Ptr) override
{
// 커스텀 해제 로직
}
};
객체 생성 및 소멸 시 메모리 관리법
- 적절한 생성 방식 선택
// UObject 파생 클래스
AMyActor* Actor = GetWorld()->SpawnActor<AMyActor>();
// 일반 C++ 클래스
FMyStruct* Struct = new FMyStruct();
- RAII(Resource Acquisition Is Initialization) 원칙 적용
class FScopedResource
{
public:
FScopedResource() { Resource = AcquireResource(); }
~FScopedResource() { ReleaseResource(Resource); }
private:
void* Resource;
};
메모리 누수 방지 기법
- 스마트 포인터 사용
TSharedPtr<FMyClass> SharedObject = MakeShared<FMyClass>();
TUniquePtr<FMyClass> UniqueObject = MakeUnique<FMyClass>();
- 가비지 컬렉션 활용
UPROPERTY()
UMyObject* ManagedObject;
- 명시적 해제 호출
FMyStruct* Struct = new FMyStruct();
// 사용 후
delete Struct;
효율적인 메모리 사용을 위한 최적화 전략
- 객체 재사용
void AMyActor::ResetObject()
{
// 객체를 삭제하고 재생성하는 대신 상태만 리셋
Health = MaxHealth;
Position = FVector::ZeroVector;
}
- 메모리 정렬
// 16바이트 정렬
UPROPERTY(Transient)
alignas(16) FVector4 AlignedVector;
대규모 데이터 구조의 메모리 관리
- TArray 최적화
TArray<int32> MyArray;
MyArray.Reserve(ExpectedSize); // 미리 메모리 할당
// 불필요한 메모리 해제
MyArray.Shrink();
- TMap 효율적 사용
TMap<FString, int32> MyMap;
MyMap.Reserve(ExpectedSize); // 미리 메모리 할당
// 키 존재 여부 확인 후 접근
if (int32* Value = MyMap.Find(Key))
{
// 키가 존재할 때만 값 사용
}
동적 메모리 할당의 성능 영향
동적 메모리 할당은 성능에 부정적인 영향을 줄 수 있습니다.
가능한 경우 정적 할당이나 메모리 풀을 사용하는 것이 좋습니다.
// 피해야 할 패턴
for (int32 i = 0; i < LargeNumber; ++i)
{
FMyStruct* Struct = new FMyStruct();
// 사용
delete Struct;
}
// 개선된 패턴
FMyStruct StructPool[PoolSize];
for (int32 i = 0; i < LargeNumber; ++i)
{
FMyStruct& Struct = StructPool[i % PoolSize];
// 사용
}
메모리 단편화 문제와 해결 방안
메모리 단편화는 성능 저하의 원인이 될 수 있습니다.
해결 방안으로 다음과 같은 기법을 사용할 수 있습니다.
- 메모리 풀 사용
- 객체 크기 표준화
- 주기적인 메모리 압축
// 메모리 압축 예시
void CompressMemory()
{
FGenericPlatformMemory::CompactMemory();
}
프로파일링 도구를 사용한 메모리 분석
언리얼 엔진의 내장 프로파일링 도구를 활용하여 메모리 사용량을 분석할 수 있습니다.
- 언리얼 프론트엔드(Unreal Frontend) 사용
- 메모리 보고서 생성
- 스태틱 메시 에디터의 메모리 사용량 분석 기능 활용
코드에서 메모리 프로파일링
// 메모리 사용량 로깅
UE_LOG(LogTemp, Warning, TEXT("Memory Used: %d bytes"), FPlatformMemory::GetStats().UsedPhysical);
멀티스레드 환경에서의 안전한 메모리 관리
- 스레드 안전한 할당자 사용
FCriticalSection CriticalSection;
void* AllocateThreadSafe(SIZE_T Size)
{
FScopeLock Lock(&CriticalSection);
return FMemory::Malloc(Size);
}
- 원자적 연산 활용
FThreadSafeCounter ObjectCount;
void IncrementCount()
{
ObjectCount.Increment();
}
- 락프리 자료구조 사용
TLockFreePointerListFIFO<FMyStruct> LockFreeList;
void AddToList(FMyStruct* Item)
{
LockFreeList.Push(Item);
}
Best Practices
1. 메모리 누수 방지
- 스마트 포인터와 RAII 원칙 적극 활용
- 가능한 경우 가비지 컬렉션 활용
2. 할당 최소화
- 객체 풀링 기법 사용
- 불필요한 임시 객체 생성 피하기
3. 캐시 친화적 설계
- 데이터 구조의 메모리 레이아웃 최적화
- 가능한 경우 연속된 메모리 사용
4. 주기적인 프로파일링
- 메모리 사용량 모니터링
- 병목 지점 식별 및 최적화
5. 안전한 멀티스레딩
- 스레드 안전한 메모리 관리 기법 사용
- 데이터 경쟁 조건 주의
효과적인 메모리 관리는 언리얼 엔진 기반 게임 개발에서 핵심적인 부분입니다.
언리얼 엔진의 내장 메모리 관리 시스템을 이해하고 활용하는 것이 중요하며 동시에 C++ 수준에서의 메모리 최적화 기법을 적용해야 합니다.