icon안동민 개발노트

프로파일링과 성능 최적화


 언리얼 엔진 C++ 프로젝트의 성능을 최적화하는 것은 게임 개발의 핵심적인 부분입니다.

 이 절에서는 프로파일링 기법과 다양한 성능 최적화 전략을 살펴보겠습니다.

언리얼 엔진의 내장 프로파일링 도구 활용

언리얼 프론트엔드 (Unreal Frontend)

 언리얼 프론트엔드는 성능 분석을 위한 강력한 도구입니다.

  1. 프로젝트 실행 > 'Profile' 옵션 선택
  2. 세션 브라우저에서 프로파일링 데이터 분석
// 프로파일링 마커 추가
SCOPE_CYCLE_COUNTER(STAT_MyFunction);
 
void AMyActor::MyFunction()
{
    // 프로파일링할 코드
}

언리얼 인사이트 (Unreal Insights)

 더 상세한 성능 분석을 위한 도구

  1. 프로젝트 설정에서 Trace 활성화
  2. 게임 실행 시 -trace=frame,cpu,gpu,memory,net 인자 추가
  3. UnrealInsights.exe로 데이터 분석
// 트레이스 이벤트 추가
TRACE_CPUPROFILER_EVENT_SCOPE(TEXT("MyCustomEvent"));
// 프로파일링할 코드

외부 프로파일링 도구 통합

 Visual Studio 프로파일러, Intel VTune 등의 외부 도구 활용

// Visual Studio 프로파일러 사용 예
##include <profileapi.h>
 
LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
LARGE_INTEGER Frequency;
 
QueryPerformanceFrequency(&Frequency); 
QueryPerformanceCounter(&StartingTime);
// 측정할 코드
QueryPerformanceCounter(&EndingTime);
ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
ElapsedMicroseconds.QuadPart *= 1000000;
ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;

성능 병목 지점 식별 및 분석

  1. 프로파일링 데이터 수집
  2. 핫스팟(자주 호출되거나 실행 시간이 긴 함수) 식별
  3. 코드 리뷰 및 최적화
// 병목 지점 예시
void AMyActor::ExpensiveFunction()
{
    SCOPE_CYCLE_COUNTER(STAT_ExpensiveFunction);
    
    for (int32 i = 0; i < 1000000; ++i)
    {
        // 비효율적인 연산
    }
}
 
// 최적화 후
void AMyActor::OptimizedFunction()
{
    SCOPE_CYCLE_COUNTER(STAT_OptimizedFunction);
    
    // 알고리즘 개선 또는 캐싱 적용
}

CPU 성능 최적화 전략

  1. 알고리즘 복잡도 감소
  2. 캐싱 활용
  3. 데이터 지향 설계(Data-Oriented Design) 적용
// 캐싱 예시
UPROPERTY()
TMap<FString, FComputedData> DataCache;
 
FComputedData AMyActor::GetComputedData(const FString& Key)
{
    if (const FComputedData* CachedData = DataCache.Find(Key))
    {
        return *CachedData;
    }
    
    FComputedData NewData = ComputeExpensiveData(Key);
    DataCache.Add(Key, NewData);
    return NewData;
}

GPU 성능 최적화 전략

  1. 드로우 콜 감소
  2. 셰이더 복잡도 최적화
  3. 텍스처 및 메시 LOD 시스템 구현
// 머티리얼 인스턴스 동적 파라미터 활용
UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(BaseMaterial, this);
DynamicMaterial->SetScalarParameterValue("Roughness", 0.5f);

메모리 성능 최적화

  1. 메모리 누수 방지
  2. 객체 풀링 구현
  3. 가비지 컬렉션 최적화
// 객체 풀링 예시
UCLASS()
class UMyObjectPool : public UObject
{
    GENERATED_BODY()
 
public:
    AMyActor* GetOrCreateActor()
    {
        if (Pool.Num() > 0)
        {
            return Pool.Pop();
        }
        return GetWorld()->SpawnActor<AMyActor>();
    }
 
    void ReturnToPool(AMyActor* Actor)
    {
        Pool.Push(Actor);
    }
 
private:
    TArray<AMyActor*> Pool;
};

네트워크 성능 최적화

  1. 대역폭 사용 최소화
  2. 패킷 압축
  3. 예측 및 보간 기법 활용
// 네트워크 최적화 예시
UCLASS()
class AMyNetworkedActor : public AActor
{
    GENERATED_BODY()
 
public:
    UPROPERTY(ReplicatedUsing = OnRep_Position)
    FVector_NetQuantize Position;
 
    UFUNCTION()
    void OnRep_Position()
    {
        // 클라이언트 측 보간 로직
    }
};

멀티스레딩 최적화 기법

  1. 작업 분할 및 병렬 처리
  2. 락 경합 최소화
  3. Task Graph 시스템 활용
// Task Graph 사용 예시
void AMyActor::PerformHeavyComputation()
{
    FAutoDeleteAsyncTask<FMyComputationTask>* MyTask = new FAutoDeleteAsyncTask<FMyComputationTask>();
    MyTask->StartBackgroundTask();
}
 
class FMyComputationTask : public FNonAbandonableTask
{
public:
    void DoWork()
    {
        // 무거운 계산 수행
    }
 
    FORCEINLINE TStatId GetStatId() const
    {
        RETURN_QUICK_DECLARE_CYCLE_STAT(FMyComputationTask, STATGROUP_ThreadPoolAsyncTasks);
    }
};

렌더링 파이프라인 최적화

  1. 오클루전 컬링 최적화
  2. 인스턴싱 활용
  3. 포스트 프로세싱 효과 최적화
// 인스턴싱 예시
UPROPERTY()
UInstancedStaticMeshComponent* InstancedMeshComponent;
 
void AMyActor::SpawnMultipleInstances(int32 Count)
{
    for (int32 i = 0; i < Count; ++i)
    {
        FTransform InstanceTransform = FTransform(FRotator::ZeroRotator, FVector(i * 100.0f, 0, 0));
        InstancedMeshComponent->AddInstance(InstanceTransform);
    }
}

대규모 오픈 월드 게임에서의 성능 최적화 전략

  1. 월드 파티셔닝 및 스트리밍 레벨 활용
  2. 거리 기반 LOD 및 컬링 시스템 구현
  3. 프로시저럴 생성 기법 활용
// 거리 기반 LOD 시스템 예시
void AMyWorldManager::UpdateLODLevels()
{
    FVector PlayerLocation = GetPlayerLocation();
    for (AActor* Actor : ManagedActors)
    {
        float Distance = FVector::Dist(PlayerLocation, Actor->GetActorLocation());
        int32 LODLevel = CalculateLODLevel(Distance);
        Cast<ILODInterface>(Actor)->SetLODLevel(LODLevel);
    }
}

동적 LOD 시스템 구현

UCLASS()
class AMyLODActor : public AActor, public ILODInterface
{
    GENERATED_BODY()
 
public:
    virtual void SetLODLevel(int32 NewLevel) override
    {
        CurrentLOD = NewLevel;
        UpdateMeshForLOD();
    }
 
private:
    void UpdateMeshForLOD();
    
    UPROPERTY()
    int32 CurrentLOD;
};

스트리밍 시스템 최적화

  1. 비동기 로딩 활용
  2. 메모리 버짓 관리
  3. 프리페치 및 예측 로딩 구현
// 비동기 레벨 스트리밍 예시
ULevelStreamingDynamic::LoadLevelInstance(GetWorld(), LevelPath, Location, Rotation, bOutSuccess, LevelPackageName);

프로파일링 데이터를 바탕으로 한 코드 리팩토링 전략

  1. 핫스팟 식별 및 최적화
  2. 데이터 구조 및 알고리즘 개선
  3. 캐싱 및 메모이제이션 적용
// 리팩토링 예시
// 최적화 전
float AMyActor::CalculateComplexValue(int32 Input)
{
    // 복잡한 계산
    return Result;
}
 
// 최적화 후
float AMyActor::CalculateComplexValue(int32 Input)
{
    static TMap<int32, float> Cache;
    if (float* CachedResult = Cache.Find(Input))
    {
        return *CachedResult;
    }
    
    float Result = /* 복잡한 계산 */;
    Cache.Add(Input, Result);
    return Result;
}

지속적인 성능 모니터링 시스템 구축

  1. 자동화된 성능 테스트 파이프라인 구축
  2. 성능 메트릭 대시보드 개발
  3. 경고 및 알림 시스템 구현
UCLASS()
class UPerformanceMonitor : public UObject
{
    GENERATED_BODY()
 
public:
    void TrackPerformanceMetric(const FString& MetricName, float Value)
    {
        PerformanceMetrics.Add(MetricName, Value);
        CheckPerformanceThresholds(MetricName, Value);
    }
 
private:
    void CheckPerformanceThresholds(const FString& MetricName, float Value);
    
    UPROPERTY()
    TMap<FString, float> PerformanceMetrics;
};

다양한 하드웨어 스펙에 대응하는 확장 가능한 성능 최적화 접근 방식

 다양한 하드웨어 환경에서 최적의 성능을 제공하기 위해서는 유연하고 확장 가능한 접근 방식이 필요합니다.

 1. 동적 품질 설정 시스템 구현

 프레임레이트와 하드웨어 성능에 따라 자동으로 그래픽 설정을 조정하는 시스템을 구현합니다.

UCLASS()
class UDynamicQualitySystem : public UObject
{
    GENERATED_BODY()
 
public:
    void UpdateQualitySettings(float CurrentFPS)
    {
        if (CurrentFPS < TargetFPS - Tolerance)
        {
            DecreaseQuality();
        }
        else if (CurrentFPS > TargetFPS + Tolerance && CurrentQualityLevel < MaxQualityLevel)
        {
            IncreaseQuality();
        }
    }
 
private:
    void DecreaseQuality();
    void IncreaseQuality();
 
    UPROPERTY(EditAnywhere, Category = "Quality Settings")
    float TargetFPS = 60.0f;
 
    UPROPERTY(EditAnywhere, Category = "Quality Settings")
    float Tolerance = 5.0f;
 
    UPROPERTY(EditAnywhere, Category = "Quality Settings")
    int32 CurrentQualityLevel = 3;
 
    UPROPERTY(EditAnywhere, Category = "Quality Settings")
    int32 MaxQualityLevel = 5;
};

 2. 하드웨어 감지 및 자동 설정 조정

 게임 시작 시 하드웨어 사양을 감지하고 초기 그래픽 설정을 자동으로 조정합니다.

UCLASS()
class UHardwareDetector : public UObject
{
    GENERATED_BODY()
 
public:
    void DetectHardwareAndAdjustSettings()
    {
        FString GPUDescription = FWindowsPlatformMisc::GetPrimaryGPUBrand();
        int32 CPUCores = FPlatformMisc::NumberOfCores();
        int64 TotalPhysicalMemory = FPlatformMemory::GetConstants().TotalPhysicalGB;
 
        EGraphicsQuality InitialQuality = DetermineInitialQuality(GPUDescription, CPUCores, TotalPhysicalMemory);
        SetInitialGraphicsSettings(InitialQuality);
    }
 
private:
    EGraphicsQuality DetermineInitialQuality(const FString& GPU, int32 CPUCores, int64 RAM);
    void SetInitialGraphicsSettings(EGraphicsQuality Quality);
};

 3. 사용자 정의 설정 옵션 제공

 사용자가 직접 성능과 품질을 조절할 수 있는 상세한 옵션을 제공합니다.

UCLASS(Config=Game)
class UGraphicsSettings : public UObject
{
    GENERATED_BODY()
 
public:
    UPROPERTY(Config, EditAnywhere, Category = "Graphics")
    TEnumAsByte<EAntiAliasingMethod> AntiAliasingMethod;
 
    UPROPERTY(Config, EditAnywhere, Category = "Graphics")
    TEnumAsByte<ETextureQuality> TextureQuality;
 
    UPROPERTY(Config, EditAnywhere, Category = "Graphics")
    TEnumAsByte<EShadowQuality> ShadowQuality;
 
    UFUNCTION(BlueprintCallable, Category = "Graphics")
    void ApplySettings()
    {
        SaveConfig();
        UpdateGraphicsSettings();
    }
 
private:
    void UpdateGraphicsSettings();
};

 4. 스케일러블 LOD 시스템

 거리와 하드웨어 성능에 따라 동적으로 LOD를 조정하는 시스템을 구현합니다.

UCLASS()
class UScalableLODSystem : public UObject
{
    GENERATED_BODY()
 
public:
    void UpdateLODLevels(UStaticMeshComponent* MeshComponent, float Distance, float PerformanceFactor)
    {
        int32 DesiredLOD = FMath::Clamp(FMath::FloorToInt(Distance / 1000.0f), 0, MeshComponent->GetNumLODs() - 1);
        DesiredLOD = FMath::Min(DesiredLOD, FMath::FloorToInt(PerformanceFactor * (MeshComponent->GetNumLODs() - 1)));
        MeshComponent->ForcedLodModel = DesiredLOD + 1;
    }
};

 5. 적응형 렌더링 해상도

 프레임레이트를 유지하기 위해 동적으로 렌더링 해상도를 조정합니다.

UCLASS()
class UAdaptiveResolution : public UObject
{
    GENERATED_BODY()
 
public:
    void UpdateResolutionScale(float CurrentFPS)
    {
        if (CurrentFPS < TargetFPS)
        {
            ResolutionScale = FMath::Max(ResolutionScale - 0.05f, MinResolutionScale);
        }
        else if (CurrentFPS > TargetFPS && ResolutionScale < 1.0f)
        {
            ResolutionScale = FMath::Min(ResolutionScale + 0.05f, 1.0f);
        }
        ApplyResolutionScale();
    }
 
private:
    void ApplyResolutionScale();
 
    UPROPERTY(EditAnywhere, Category = "Resolution")
    float ResolutionScale = 1.0f;
 
    UPROPERTY(EditAnywhere, Category = "Resolution")
    float MinResolutionScale = 0.5f;
 
    UPROPERTY(EditAnywhere, Category = "Resolution")
    float TargetFPS = 60.0f;
};

 6. 멀티스레딩 최적화

 하드웨어의 코어 수에 따라 동적으로 작업을 분배합니다.

UCLASS()
class UAdaptiveMultithreading : public UObject
{
    GENERATED_BODY()
 
public:
    void DistributeWorkload(const TArray<FWorkItem>& WorkItems)
    {
        int32 NumThreads = FPlatformMisc::NumberOfWorkerThreads();
        int32 ItemsPerThread = FMath::CeilToInt(static_cast<float>(WorkItems.Num()) / NumThreads);
 
        for (int32 i = 0; i < NumThreads; ++i)
        {
            int32 StartIndex = i * ItemsPerThread;
            int32 EndIndex = FMath::Min((i + 1) * ItemsPerThread, WorkItems.Num());
            AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, WorkItems, StartIndex, EndIndex]()
            {
                ProcessWorkItems(WorkItems, StartIndex, EndIndex);
            });
        }
    }
 
private:
    void ProcessWorkItems(const TArray<FWorkItem>& WorkItems, int32 StartIndex, int32 EndIndex);
};

 이러한 접근 방식을 통해 다양한 하드웨어 환경에서 최적의 성능과 시각적 품질을 제공할 수 있습니다. 동적 품질 설정 시스템은 실시간으로 성능을 모니터링하고 조정하여 일관된 프레임레이트를 유지합니다. 하드웨어 감지 기능은 게임 시작 시 최적의 초기 설정을 제공하며, 사용자 정의 옵션을 통해 플레이어가 자신의 선호도에 따라 설정을 조정할 수 있습니다.

 스케일러블 LOD 시스템과 적응형 렌더링 해상도는 다양한 성능 수준의 하드웨어에서 시각적 품질과 성능 간의 균형을 유지하는 데 도움이 됩니다. 멀티스레딩 최적화는 다중 코어 프로세서를 효율적으로 활용하여 전반적인 성능을 향상시킵니다.

 이러한 시스템을 구현할 때는 사용자 경험을 최우선으로 고려해야 합니다. 품질 변화가 너무 급격하거나 빈번하지 않도록 주의하며, 사용자에게 변경 사항을 명확히 알리고 필요한 경우 수동으로 조정할 수 있는 옵션을 제공해야 합니다.

 또한, 이러한 시스템의 효과를 지속적으로 모니터링하고 분석하여 개선점을 찾아야 합니다. 사용자 피드백과 성능 데이터를 수집하고 분석하여 시스템을 지속적으로 개선하고 최적화하는 것이 중요합니다.

 마지막으로, 새로운 하드웨어 출시와 드라이버 업데이트에 대응할 수 있도록 시스템을 유연하게 설계해야 합니다. 정기적인 업데이트를 통해 최신 하드웨어와 소프트웨어 변경사항을 반영하고, 필요한 경우 새로운 최적화 기법을 도입해야 합니다.