CPU, GPU 병목 현상 해결
stat unit으로 Game·Render·RHI·GPU 시간을 분리하고 Insights와 profilegpu로 원인을 좁힙니다.
이전 절들에서 우리는 성능 프로파일링 도구 사용법과 메모리 및 렌더링 최적화 기법에 대해 자세히 알아보았습니다.
이제는 게임 성능 저하의 원인이 Game Thread, Render Thread, RHI Thread, GPU 중 어디에 가까운지 분리하고 대응하는 전략을 정리합니다.
프레임 시간은 여러 축 중 가장 늦게 끝나는 작업의 영향을 받으므로, FPS만 보고 원인을 단정하지 않습니다.
이번 절에서는 각 병목 신호의 특성을 이해하고, 측정값과 수정 범위를 연결하는 접근 방식을 다룹니다.
병목 현상 진단: CPU vs GPU
stat unit으로 긴 프레임을 분류한 다음, 해당 작업의 타임라인을 조사합니다.
Frame과 함께 증가하는 축은 원인을 찾기 위한 후보입니다.
| 신호 | 다음 확인 | 수정 전에 구별할 것 |
|---|---|---|
| Game 증가 | Timing Insights에서 Tick·AI·물리 요청·애니메이션 호출을 찾습니다. | 계산 시간과 다른 작업을 기다린 시간을 구별합니다. |
| Draw·RHIT 증가 | 렌더·RHI 타임라인과 제출·상태 변경 통계를 확인합니다. | CPU 명령 준비·제출과 GPU 셰이더 실행 비용은 다른 축입니다. |
| GPU 증가 | GPU 패스에서 그림자·투명·BasePass·후처리 시간을 비교합니다. | Shader Complexity의 색은 실제 GPU 밀리초가 아닙니다. |
| Frame만 길거나 순간 끊김 | 프레임 제한·VSync·동기화·로딩과 메모리 추적을 함께 확인합니다. | 프레임 평균과 드문 스파이크를 나눕니다. Frame만으로 특정 함수의 원인을 확정하지 않습니다. |
- Game 증가
- 다음 확인: Timing Insights에서 Tick·AI·물리 요청·애니메이션 호출을 찾습니다.수정 전에 구별할 것: 계산 시간과 다른 작업을 기다린 시간을 구별합니다.
- Draw·RHIT 증가
- 다음 확인: 렌더·RHI 타임라인과 제출·상태 변경 통계를 확인합니다.수정 전에 구별할 것: CPU 명령 준비·제출과 GPU 셰이더 실행 비용은 다른 축입니다.
- GPU 증가
- 다음 확인: GPU 패스에서 그림자·투명·BasePass·후처리 시간을 비교합니다.수정 전에 구별할 것: Shader Complexity의 색은 실제 GPU 밀리초가 아닙니다.
- Frame만 길거나 순간 끊김
- 다음 확인: 프레임 제한·VSync·동기화·로딩과 메모리 추적을 함께 확인합니다.수정 전에 구별할 것: 프레임 평균과 드문 스파이크를 나눕니다. Frame만으로 특정 함수의 원인을 확정하지 않습니다.
동일한 맵·카메라·해상도·빌드로 여러 프레임을 비교합니다. 여기에는 실제 측정 수치가 제시되어 있지 않습니다.
CPU 병목 현상 해결
CPU 병목 현상은 게임 로직 또는 렌더링 명령 준비 과정에서 CPU가 과도하게 사용될 때 발생합니다.
Game Thread 최적화
Game Thread는 게임플레이 로직의 대부분을 처리합니다.
이곳에서 병목이 발생하면 게임 업데이트가 느려져 프레임이 떨어집니다.
해결 전략-
액터 및 컴포넌트 업데이트 최적화
- Tick 함수 최소화:
AActor::Tick()및UActorComponent::TickComponent()함수는 매 프레임 실행되므로, 이곳에 무거운 로직을 넣지 않도록 주의합니다. 생성자에서PrimaryActorTick.bCanEverTick또는PrimaryComponentTick.bCanEverTick을 false로 두어 불필요한 틱을 등록하지 않습니다. 이미 등록된 틱의 실행 여부는 해당 TickEnabled API로 조절합니다. - 틱 그룹(Tick Groups) 활용: 물리 전후 등 필요한 단계와 선행 관계를 지정합니다. 그룹은 중요도 순위가 아니며 같은 그룹의 임의 액터 순서를 보장하지 않으므로 의존성이 있으면 prerequisite를 설정합니다.
- 틱 간격 조절:
SetActorTickInterval()을 사용하여 특정 액터가 매 프레임이 아닌, 예를 들어 0.1초마다 한 번씩만 틱되도록 설정합니다. 멀리 있는 NPC나 환경 오브젝트에 유용합니다. - 컴포넌트 단위 최적화: 액터 전체가 아닌 필요한 컴포넌트만 틱하도록 설정하고, 불필요한 컴포넌트는 비활성화합니다.
- Tick 함수 최소화:
-
물리 시뮬레이션 최적화
- 물리 바디 수 줄이기: 복잡한 물리 시뮬레이션은 CPU 자원을 많이 소모합니다. 불필요한 물리 오브젝트는
Simulate Physics를 끄거나, Collision Preset을No Collision으로 설정합니다. - 콜리전 복잡도 단순화: 시뮬레이션 대상은
UCapsuleComponent,UBoxComponent같은 단순한 프리미티브 콜리전이나 단순 콜리전 메시를 우선 사용합니다.Use Complex as Simple은 물리 시뮬레이션 대상의 일반적인 최적화 해법으로 보면 안 되며, 정적 쿼리 용도 등 제한된 맥락에서만 검토합니다. - Physics/Chaos 설정 점검: 프로젝트의 엔진 버전에 맞는 물리 백엔드와 substepping, 충돌 채널, 물리 바디 수를 함께 조정합니다.
- 물리 바디 수 줄이기: 복잡한 물리 시뮬레이션은 CPU 자원을 많이 소모합니다. 불필요한 물리 오브젝트는
-
AI 및 Pathfinding 최적화
- 거리 기반 AI 비활성화: 플레이어로부터 멀리 떨어진 AI는 업데이트 빈도를 줄이거나 완전히 비활성화합니다. Behavior Tree의
Services나Decorators에서 조건부로 AI 로직을 실행하도록 합니다. - Pathfinding 복잡도: 복잡한 내비게이션 메시(Navigation Mesh) 생성 비용을 줄이고,
RecastNavMesh설정에서 셀 크기 등을 조절합니다. - AI 병렬 처리: 일부 AI 시스템은 멀티스레딩을 활용하여 CPU 코어를 분산하여 사용합니다.
- 거리 기반 AI 비활성화: 플레이어로부터 멀리 떨어진 AI는 업데이트 빈도를 줄이거나 완전히 비활성화합니다. Behavior Tree의
-
애니메이션 최적화
- LOD (Level Of Detail): 스켈레탈 메시의 LOD를 설정하여 거리에 따라 더 적은 본(Bone)과 복잡도로 애니메이션을 계산합니다.
- 애니메이션 블루프린트 복잡도: 애니메이션 블루프린트의
Event Graph와Anim 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): 여러 작은 텍스처를 하나의 큰 텍스처에 묶어 사용하여 텍스처 바인딩 및 셰이더 변경 비용을 줄입니다.
- 인스턴싱(Instancing): 동일한 메시를 여러 번 배치하는 경우
-
컬링(Culling) 최적화
- 오클루전 컬링 (Occlusion Culling): 시야에 가려진 오브젝트를 렌더링하지 않도록 합니다. 언리얼 엔진은 기본적으로 하드웨어 오클루전 컬링을 사용합니다.
- Precomputed Visibility Volume: 정적인 씬에서 미리 계산된 가시성 데이터를 사용하여 런타임 컬링 효율을 높입니다.
- Foliage 및 Instanced Static Mesh 컬링: 인스턴싱된 오브젝트의 컬링 설정을 조정하여 불필요한 렌더링을 줄입니다.
-
광원 및 그림자 최적화
- 동적 광원 수 제한: 동적 광원은 드로우 콜과 셰이더 복잡도를 증가시킵니다. 사용 중인 조명 경로에서 영향 범위와 그림자 비용을 비교합니다. 베이크 조명과 달리 Lumen GI에서는 Static 광원의 사전 계산 기여가 비활성화됩니다.
- 그림자 최적화:
Cast Shadows비활성화, 그림자 맵 해상도 감소, 캐스케이드 수 조절 등 렌더링 최적화 절에서 다룬 기법들을 적용합니다.
- 렌더 상태 변경 줄이기: Render Thread에서는 primitive/component 수, 드로우 제출, 머티리얼 전환, 동적 상태 변경을 확인합니다. 머티리얼 셰이더 실행 비용은 주로 GPU 항목에서
Shader Complexity와profilegpu로 따로 봅니다.
GPU 병목 현상 해결
GPU 병목 현상은 GPU가 픽셀 처리, 정점 처리, 셰이더 계산 등 실제 렌더링 작업을 수행하는 데 시간이 오래 걸릴 때 발생합니다.
profilegpu 명령어가 이 문제를 진단하는 데 가장 유용합니다.
-
픽셀 복잡도 감소 (핵심!)
- 오버드로우(Overdraw) 감소: 투명 또는 마스크드 머티리얼 사용을 최소화하고, 파티클 시스템의 오버드로우를 최적화합니다.
Shader Complexity모드를 사용하여 과도한 오버드로우 영역을 찾습니다. - 머티리얼 셰이더 복잡도: 머티리얼 에디터의
Stats탭을 확인하여 셰이더 명령 수를 줄입니다. 불필요한 연산, 텍스처 샘플링을 제거합니다. - 렌더링 기능 비활성화: 프로젝트 설정(
Project Settings->Rendering)에서 필요 없는 렌더링 기능(예:Motion Blur,Bloom,Ambient Occlusion등)을 비활성화하거나 품질을 낮춥니다.
- 오버드로우(Overdraw) 감소: 투명 또는 마스크드 머티리얼 사용을 최소화하고, 파티클 시스템의 오버드로우를 최적화합니다.
-
폴리곤 수 감소
- 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와 컬링을 활용합니다.
기타 고려 사항 및 고급 기법
- 멀티스레딩 활용: 언리얼 엔진은 기본적으로 멀티스레딩 아키텍처를 사용하지만, 특정 계산을
FAsyncTask나Async로 분리해 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등에서 전반적인 품질 설정을 조정할 수 있습니다. - 콘텐츠 드라이븐 최적화: 개발 단계부터 에셋 제작 가이드라인을 설정하여 최적화된 모델, 텍스처, 머티리얼을 제작하도록 유도합니다.
변경 뒤에는 같은 장면에서 Frame·Game·Draw·RHIT·GPU를 다시 기록합니다. 개선 폭뿐 아니라 병목이 다른 작업으로 옮겨갔는지도 확인합니다.