icon안동민 개발노트

메모리 관리 기초


 언리얼 엔진에서의 효과적인 메모리 관리는 게임의 성능과 안정성을 위해 중요합니다.

 이 절에서는 C++ 프로그래밍 관점에서 언리얼 엔진의 메모리 관리 기법과 주의사항을 살펴보겠습니다.

언리얼 엔진의 메모리 할당자 시스템

 언리얼 엔진은 자체적인 메모리 할당자 시스템을 제공합니다.

 이 시스템은 다음과 같은 특징을 가집니다.

  1. 메모리 풀링
  2. 멀티스레드 지원
  3. 메모리 단편화 최소화

 기본 메모리 할당자 사용 예

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
    {
        // 커스텀 해제 로직
    }
};

객체 생성 및 소멸 시 메모리 관리법

  1. 적절한 생성 방식 선택
// UObject 파생 클래스
AMyActor* Actor = GetWorld()->SpawnActor<AMyActor>();
 
// 일반 C++ 클래스
FMyStruct* Struct = new FMyStruct();
  1. RAII(Resource Acquisition Is Initialization) 원칙 적용
class FScopedResource
{
public:
    FScopedResource() { Resource = AcquireResource(); }
    ~FScopedResource() { ReleaseResource(Resource); }
 
private:
    void* Resource;
};

메모리 누수 방지 기법

  1. 스마트 포인터 사용
TSharedPtr<FMyClass> SharedObject = MakeShared<FMyClass>();
TUniquePtr<FMyClass> UniqueObject = MakeUnique<FMyClass>();
  1. 가비지 컬렉션 활용
UPROPERTY()
UMyObject* ManagedObject;
  1. 명시적 해제 호출
FMyStruct* Struct = new FMyStruct();
// 사용 후
delete Struct;

효율적인 메모리 사용을 위한 최적화 전략

  1. 객체 재사용
void AMyActor::ResetObject()
{
    // 객체를 삭제하고 재생성하는 대신 상태만 리셋
    Health = MaxHealth;
    Position = FVector::ZeroVector;
}
  1. 메모리 정렬
// 16바이트 정렬
UPROPERTY(Transient)
alignas(16) FVector4 AlignedVector;

대규모 데이터 구조의 메모리 관리

  1. TArray 최적화
TArray<int32> MyArray;
MyArray.Reserve(ExpectedSize);  // 미리 메모리 할당
 
// 불필요한 메모리 해제
MyArray.Shrink();
  1. 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];
    // 사용
}

메모리 단편화 문제와 해결 방안

 메모리 단편화는 성능 저하의 원인이 될 수 있습니다.

 해결 방안으로 다음과 같은 기법을 사용할 수 있습니다.

  1. 메모리 풀 사용
  2. 객체 크기 표준화
  3. 주기적인 메모리 압축
// 메모리 압축 예시
void CompressMemory()
{
    FGenericPlatformMemory::CompactMemory();
}

프로파일링 도구를 사용한 메모리 분석

 언리얼 엔진의 내장 프로파일링 도구를 활용하여 메모리 사용량을 분석할 수 있습니다.

  1. 언리얼 프론트엔드(Unreal Frontend) 사용
  2. 메모리 보고서 생성
  3. 스태틱 메시 에디터의 메모리 사용량 분석 기능 활용

 코드에서 메모리 프로파일링

// 메모리 사용량 로깅
UE_LOG(LogTemp, Warning, TEXT("Memory Used: %d bytes"), FPlatformMemory::GetStats().UsedPhysical);

멀티스레드 환경에서의 안전한 메모리 관리

  1. 스레드 안전한 할당자 사용
FCriticalSection CriticalSection;
 
void* AllocateThreadSafe(SIZE_T Size)
{
    FScopeLock Lock(&CriticalSection);
    return FMemory::Malloc(Size);
}
  1. 원자적 연산 활용
FThreadSafeCounter ObjectCount;
 
void IncrementCount()
{
    ObjectCount.Increment();
}
  1. 락프리 자료구조 사용
TLockFreePointerListFIFO<FMyStruct> LockFreeList;
 
void AddToList(FMyStruct* Item)
{
    LockFreeList.Push(Item);
}

Best Practices

 1. 메모리 누수 방지

  • 스마트 포인터와 RAII 원칙 적극 활용
  • 가능한 경우 가비지 컬렉션 활용

 2. 할당 최소화

  • 객체 풀링 기법 사용
  • 불필요한 임시 객체 생성 피하기

 3. 캐시 친화적 설계

  • 데이터 구조의 메모리 레이아웃 최적화
  • 가능한 경우 연속된 메모리 사용

 4. 주기적인 프로파일링

  • 메모리 사용량 모니터링
  • 병목 지점 식별 및 최적화

 5. 안전한 멀티스레딩

  • 스레드 안전한 메모리 관리 기법 사용
  • 데이터 경쟁 조건 주의

 효과적인 메모리 관리는 언리얼 엔진 기반 게임 개발에서 핵심적인 부분입니다.

 언리얼 엔진의 내장 메모리 관리 시스템을 이해하고 활용하는 것이 중요하며 동시에 C++ 수준에서의 메모리 최적화 기법을 적용해야 합니다.