안동민 개발노트 아이콘

안동민 개발노트

9장 : 성능 최적화

CPU, GPU 병목 현상 해결

이전 절들에서 우리는 성능 프로파일링 도구 사용법과 메모리 및 렌더링 최적화 기법에 대해 자세히 알아보았습니다. 이제는 게임 성능 저하의 원인이 Game Thread, Render Thread, RHI Thread, GPU 중 어디에 가까운지 분리하고 대응하는 전략을 정리합니다. 프레임 시간은 여러 축 중 가장 늦게 끝나는 작업의 영향을 받으므로, FPS만 보고 원인을 단정하지 않습니다.

이번 절에서는 각 병목 신호의 특성을 이해하고, 측정값과 수정 범위를 연결하는 접근 방식을 다룹니다.


병목 현상 진단: CPU vs GPU

CPU/GPU 병목은 먼저 stat unit으로 축을 나누고, 이후 Unreal Insights나 profilegpu로 원인을 좁힙니다.

최적화의 첫걸음은 병목 후보를 나누는 것입니다. 언리얼 엔진에서 1차 분류에 자주 쓰는 도구는 stat unit 콘솔 명령어입니다.

  • stat unit 결과 해석
    • Frame: 전체 프레임 시간입니다. 대응 축이 아니라 결과값으로 봅니다.
    • Game: CPU의 게임 스레드(Game Thread) 시간입니다. 액터 업데이트, 물리 요청, AI, 애니메이션 호출, 블루프린트/C++ 호출 경계 등을 의심합니다.
    • Draw: CPU의 렌더 스레드(Render Thread) 시간입니다. 렌더 명령 생성, primitive/component 수, render state 변경, draw submission을 의심합니다.
    • GPU: GPU 렌더링 시간입니다. 픽셀 처리, 셰이더, 그림자, 후처리, 오버드로우를 profilegpu와 함께 확인합니다. 동기화나 idle이 섞일 수 있어 단독으로 길고 Frame과 가까운지 봅니다.
    • RHIT: RHI 스레드 시간입니다. 그래픽 API 제출, 리소스 전환, 동기화 대기 가능성을 확인합니다.

stat unit은 해결책을 자동으로 고르는 도구가 아닙니다. Frame 시간이 어느 축과 가까운지 보고 후보를 고른 뒤, Unreal Insights, stat game, stat gpu, profilegpu로 실제 호출 위치를 확인합니다.

아래 기준표는 stat unit, stat gpu, Unreal Insights에서 관찰한 신호를 실제 대응책으로 연결하는 기준입니다.


CPU 병목 현상 해결

CPU 병목 현상은 게임 로직 또는 렌더링 명령 준비 과정에서 CPU가 과도하게 사용될 때 발생합니다.

Game Thread 최적화

Game Thread는 게임플레이 로직의 대부분을 처리합니다. 이곳에서 병목이 발생하면 게임 업데이트가 느려져 프레임이 떨어집니다.

해결 전략
  • 액터 및 컴포넌트 업데이트 최적화
    • Tick 함수 최소화: AActor::Tick()UActorComponent::Tick() 함수는 매 프레임 실행되므로, 이곳에 무거운 로직을 넣지 않도록 주의합니다. bCanEverTickfalse로 설정하여 불필요한 틱을 비활성화합니다.
    • 틱 그룹(Tick Groups) 활용: 특정 액터의 틱 업데이트 순서를 제어하고, 중요한 액터가 먼저 업데이트되도록 하거나, 덜 중요한 액터는 더 늦게 업데이트되도록 조정합니다.
    • 틱 간격 조절: SetActorTickInterval()을 사용하여 특정 액터가 매 프레임이 아닌, 예를 들어 0.1초마다 한 번씩만 틱되도록 설정합니다. 멀리 있는 NPC나 환경 오브젝트에 유용합니다.
    • 컴포넌트 단위 최적화: 액터 전체가 아닌 필요한 컴포넌트만 틱하도록 설정하고, 불필요한 컴포넌트는 비활성화합니다.
  • 물리 시뮬레이션 최적화
    • 물리 바디 수 줄이기: 복잡한 물리 시뮬레이션은 CPU 자원을 많이 소모합니다. 불필요한 물리 오브젝트는 Simulate Physics를 끄거나, Collision Preset을 No Collision으로 설정합니다.
    • 콜리전 복잡도 단순화: 시뮬레이션 대상은 UCapsuleComponent, UBoxComponent 같은 단순한 프리미티브 콜리전이나 단순 콜리전 메시를 우선 사용합니다. Use Complex as Simple은 물리 시뮬레이션 대상의 일반적인 최적화 해법으로 보면 안 되며, 정적 쿼리 용도 등 제한된 맥락에서만 검토합니다.
    • Physics/Chaos 설정 점검: 프로젝트의 엔진 버전에 맞는 물리 백엔드와 substepping, 충돌 채널, 물리 바디 수를 함께 조정합니다.
  • AI 및 Pathfinding 최적화
    • 거리 기반 AI 비활성화: 플레이어로부터 멀리 떨어진 AI는 업데이트 빈도를 줄이거나 완전히 비활성화합니다. Behavior Tree의 ServicesDecorators에서 조건부로 AI 로직을 실행하도록 합니다.
    • Pathfinding 복잡도: 복잡한 내비게이션 메시(Navigation Mesh) 생성 비용을 줄이고, RecastNavMesh 설정에서 셀 크기 등을 조절합니다.
    • AI 병렬 처리: 일부 AI 시스템은 멀티스레딩을 활용하여 CPU 코어를 분산하여 사용합니다.
  • 애니메이션 최적화
    • LOD (Level Of Detail): 스켈레탈 메시의 LOD를 설정하여 거리에 따라 더 적은 본(Bone)과 복잡도로 애니메이션을 계산합니다.
    • 애니메이션 블루프린트 복잡도: 애니메이션 블루프린트의 Event GraphAnim Graph에서 복잡한 로직을 최소화합니다. Evaluate Graph 노드의 비용을 확인하고 줄입니다.
    • 애니메이션 컬링: 시야 밖에 있는 캐릭터나 작은 캐릭터의 애니메이션 업데이트를 중단하거나 최소화합니다.
    • Pose Asset 및 Anim Montages: 효율적인 애니메이션 블렌딩을 위해 사용합니다.
  • 오브젝트 풀링: 총알, 파티클 효과, 임시 액터 등 자주 생성되고 파괴되는 오브젝트는 미리 생성해두고 재활용하는 오브젝트 풀링을 구현하여 SpawnActor/DestroyActor 호출로 인한 CPU 오버헤드를 줄입니다.
  • 블루프린트 → C++ 마이그레이션: 프로파일링으로 hot path가 확인된 로직은 C++로 옮기는 것을 검토합니다. 단순히 C++로 바꾸는 것보다 호출 빈도, 자료구조, UObject 접근 방식이 더 큰 영향을 줄 수 있습니다.

Render Thread 최적화

Render Thread는 게임 월드를 렌더링하기 위한 명령을 준비하고 그래픽 API(RHI)로 전송하는 역할을 합니다. 이곳에서 병목이 발생하면 드로우 콜(Draw Calls) 수가 많거나 렌더링 상태 변경이 잦을 가능성이 큽니다.

해결 전략
  • 드로우 제출 비용 감소
    • 인스턴싱(Instancing): 동일한 메시를 여러 번 배치하는 경우 UInstancedStaticMeshComponent 또는 UFoliageType을 사용해 제출 비용을 줄일 수 있습니다. 머티리얼 섹션, 인스턴스 컬링, 플랫폼 제약에 따라 효과는 달라집니다.
    • 액터 머지(Actor Merging): 에디터의 Merge Actors 기능을 사용하여 인접한 여러 스태틱 메시를 하나의 메시로 병합합니다. 다만 머티리얼 수가 많거나 오클루전/LOD 단위가 커지면 이득이 줄 수 있습니다.
    • HLOD (Hierarchical Level of Detail): 대규모 오픈 월드에서 멀리 있는 여러 액터들을 하나의 최적화된 메시와 머티리얼로 자동 병합하여 드로우 콜을 줄입니다.
    • 텍스처 아틀라스(Texture Atlases): 여러 작은 텍스처를 하나의 큰 텍스처에 묶어 사용하여 텍스처 바인딩 및 셰이더 변경 비용을 줄입니다.
  • 컬링(Culling) 최적화
    • 오클루전 컬링 (Occlusion Culling): 시야에 가려진 오브젝트를 렌더링하지 않도록 합니다. 언리얼 엔진은 기본적으로 하드웨어 오클루전 컬링을 사용합니다.
    • Precomputed Visibility Volume: 정적인 씬에서 미리 계산된 가시성 데이터를 사용하여 런타임 컬링 효율을 높입니다.
    • Foliage 및 Instanced Static Mesh 컬링: 인스턴싱된 오브젝트의 컬링 설정을 조정하여 불필요한 렌더링을 줄입니다.
  • 광원 및 그림자 최적화
    • 동적 광원 수 제한: 동적 광원은 드로우 콜과 셰이더 복잡도를 증가시킵니다. 가능한 한 Static 또는 Stationary 광원을 사용하고, Movable 광원 수는 최소화합니다.
    • 그림자 최적화: Cast Shadows 비활성화, 그림자 맵 해상도 감소, 캐스케이드 수 조절 등 렌더링 최적화 절에서 다룬 기법들을 적용합니다.
  • 렌더 상태 변경 줄이기: Render Thread에서는 primitive/component 수, 드로우 제출, 머티리얼 전환, 동적 상태 변경을 확인합니다. 머티리얼 셰이더 실행 비용은 주로 GPU 항목에서 Shader Complexityprofilegpu로 따로 봅니다.

GPU 병목 현상 해결

GPU 병목 현상은 GPU가 픽셀 처리, 정점 처리, 셰이더 계산 등 실제 렌더링 작업을 수행하는 데 시간이 오래 걸릴 때 발생합니다. profilegpu 명령어가 이 문제를 진단하는 데 가장 유용합니다.

해결 전략
  • 픽셀 복잡도 감소 (핵심!)
    • 오버드로우(Overdraw) 감소: 투명 또는 마스크드 머티리얼 사용을 최소화하고, 파티클 시스템의 오버드로우를 최적화합니다. Shader Complexity 모드를 사용하여 과도한 오버드로우 영역을 찾습니다.
    • 머티리얼 셰이더 복잡도: 머티리얼 에디터의 Stats 탭을 확인하여 셰이더 명령 수를 줄입니다. 불필요한 연산, 텍스처 샘플링을 제거합니다.
    • 렌더링 기능 비활성화: 프로젝트 설정(Project Settings -> Rendering)에서 필요 없는 렌더링 기능(예: Motion Blur, Bloom, Ambient Occlusion 등)을 비활성화하거나 품질을 낮춥니다.
  • 폴리곤 수 감소
    • LOD 적용: 렌더링 최적화 절에서 다룬 스태틱 및 스켈레탈 메시의 LOD를 사용하여 원거리 오브젝트의 폴리곤 수를 줄입니다.
    • 최적화된 모델링: 에셋 제작 단계에서 폴리곤 수를 최소화하고 노멀 맵으로 디테일을 표현합니다.
  • 해상도 및 스케일링 조절
    • 화면 해상도: 게임의 기본 해상도를 낮추거나, r.ScreenPercentage 콘솔 변수를 사용하여 렌더링 해상도를 줄이면 GPU 부하를 크게 낮출 수 있습니다. (시각적 품질 저하가 발생할 수 있음)
    • 업스케일링 기술: FSR (FidelityFX Super Resolution), DLSS (Deep Learning Super Sampling)와 같은 업스케일링 기술을 적용하여 낮은 해상도로 렌더링하고 고해상도로 출력하여 GPU 부담을 줄이면서도 시각적 품질을 유지합니다.
  • 그림자 맵 품질 및 종류 조절: r.Shadow.MaxResolution, r.Shadow.TexelsPerPixel, 캐스케이드 수 등을 조절하여 그림자 렌더링 비용을 낮춥니다. 실시간 레이 트레이싱 그림자 대신 라이트맵이나 캡슐 그림자를 고려합니다.
  • 후처리 효과 최적화: Post Process Volume에서 각 후처리 효과의 품질 설정을 조정합니다. 복잡한 커스텀 후처리 셰이더를 사용할 때는 성능을 면밀히 검토해야 합니다.
  • 파티클 시스템 최적화: 과도한 수의 파티클, 복잡한 파티클 머티리얼, 높은 오버드로우를 유발하는 파티클은 피합니다. LOD와 컬링을 활용합니다.

기타 고려 사항 및 고급 기법

  • 멀티스레딩 활용: 언리얼 엔진은 기본적으로 멀티스레딩 아키텍처를 사용하지만, 특정 계산을 FAsyncTaskAsync로 분리해 Game Thread 부담을 줄일 수 있습니다. 다만 UObject 생성/변경, Actor/Component 접근은 Game Thread 제약을 지켜야 하며, 작업 완료 후 필요한 변경은 Game Thread로 돌려보내야 합니다.
  • 데이터 지향 설계 (Data-Oriented Design, DOD): 캐시 효율을 높이기 위해 데이터를 메모리에 연속적으로 배치하고, 유사한 데이터를 함께 처리하여 CPU의 캐시 미스(Cache Miss)를 줄이는 설계 방식입니다. 언리얼 엔진의 ECS (Entity Component System) 유사 기능인 Mass Framework 등이 이에 해당합니다.
  • 프로젝트 설정 조정: Project Settings -> Engine -> Rendering 또는 Physics 등에서 전반적인 품질 설정을 조정할 수 있습니다.
  • 콘텐츠 드라이븐 최적화: 개발 단계부터 에셋 제작 가이드라인을 설정하여 최적화된 모델, 텍스처, 머티리얼을 제작하도록 유도합니다.

CPU와 GPU 병목 현상은 stat unit으로 후보 축을 나눈 뒤, Game Thread는 Unreal Insights와 stat game, Render/RHI는 렌더 제출 신호, GPU는 profilegpu와 뷰 모드로 원인을 좁힙니다. 수정 뒤에는 같은 장면에서 Frame, Game, Draw, RHIT, GPU가 어떻게 이동했는지 다시 기록해야 합니다.


stat unit의 Game, Draw, GPU 값을 먼저 읽으면 최적화 대상을 추측하지 않고 좁힐 수 있습니다.

병목 해결은 Frame과 가까운 축을 고르는 것에서 시작하지만, 변경 뒤에는 다른 스레드나 GPU로 병목이 옮겨가지 않았는지 다시 확인해야 합니다.

CPU/GPU 병목 해결은 측정 지표, 병목 주체, 변경 범위, 재측정 기준으로 정리합니다.