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 원칙 적극 활용
  • 가능한 경우 가비지 컬렉션 활용
  1. 할당 최소화
  • 객체 풀링 기법 사용
  • 불필요한 임시 객체 생성 피하기
  1. 캐시 친화적 설계
  • 데이터 구조의 메모리 레이아웃 최적화
  • 가능한 경우 연속된 메모리 사용
  1. 주기적인 프로파일링
  • 메모리 사용량 모니터링
  • 병목 지점 식별 및 최적화
  1. 안전한 멀티스레딩
  • 스레드 안전한 메모리 관리 기법 사용
  • 데이터 경쟁 조건 주의

 효과적인 메모리 관리는 언리얼 엔진 기반 게임 개발에서 핵심적인 부분입니다. 언리얼 엔진의 내장 메모리 관리 시스템을 이해하고 활용하는 것이 중요하며, 동시에 C++ 수준에서의 메모리 최적화 기법을 적용해야 합니다.

 대규모 프로젝트에서는 메모리 사용량을 지속적으로 모니터링하고 최적화하는 것이 필수적입니다. 객체 풀링, 효율적인 데이터 구조 사용, 그리고 불필요한 동적 할당 최소화 등의 기법을 통해 메모리 사용을 최적화할 수 있습니다.

 멀티스레드 환경에서는 스레드 안전성을 고려한 메모리 관리가 중요합니다. 락프리 알고리즘, 원자적 연산, 그리고 적절한 동기화 메커니즘을 사용하여 데이터 경쟁 조건을 방지해야 합니다.

 마지막으로, 프로파일링 도구를 적극 활용하여 메모리 사용 패턴을 분석하고, 지속적인 최적화를 수행하는 것이 중요합니다. 이를 통해 게임의 성능과 안정성을 크게 향상시킬 수 있습니다.