1. 프로파일링이 필요한 이유
게임을 개발하다 보면 "왜 프레임이 떨어지는가?"라는 질문에 직면하게 됩니다. 단순히 FPS 숫자만 보고 최적화를 시도하면, 실제 병목이 아닌 곳에 시간을 낭비하기 쉽습니다. 게임 스레드가 문제인지, 렌더 스레드가 문제인지, 아니면 GPU가 문제인지를 먼저 판별해야 올바른 방향으로 최적화할 수 있습니다.
언리얼 엔진 5에서는 이러한 성능 분석을 위해 Unreal Insights라는 독립 실행형 프로파일링 도구를 제공합니다. 기존 UE4 시절의 Unreal Frontend 내장 프로파일러를 대체하는 도구로, CPU 스레드별 타이밍, GPU 처리 시간, 메모리 사용량, 네트워크 대역폭 등을 정밀하게 추적하고 시각화할 수 있습니다. 프레임 단위로 어떤 함수가 얼마나 시간을 소모하는지, 스레드 간 대기가 어디서 발생하는지를 한눈에 파악할 수 있어서, 최적화의 출발점으로 삼기에 가장 적합한 도구입니다.
2. 언리얼 엔진의 스레드 구조 이해
Unreal Insights를 효과적으로 활용하려면, 먼저 언리얼 엔진의 멀티스레드 구조를 이해해야 합니다. 언리얼 엔진 5는 크게 세 개의 핵심 스레드로 프레임을 처리합니다.
2-1. Game Thread
게임 로직의 중심이 되는 스레드입니다. Tick 함수를 통한 액터 업데이트, 블루프린트 실행, 물리 시뮬레이션 트리거, AI 로직, 입력 처리 등이 모두 이 스레드에서 수행됩니다. 게임 스레드가 느려지면 프레임 전체가 지연됩니다.
2-2. Render Thread (Draw Thread)
Game Thread가 생성한 렌더링 명령을 받아 플랫폼에 독립적인 그래픽 명령으로 변환하는 프론트엔드 역할을 합니다. 가시성 판정(Visibility Culling), 드로우 콜 생성, 셰도우 계산 등을 담당합니다. 일반적으로 Game Thread보다 한 프레임 뒤에서 동작합니다.
2-3. RHI Thread
Render Thread가 생성한 플랫폼 독립적 명령을 실제 그래픽 API(DX12, Vulkan 등)의 명령으로 변환하여 GPU에 전달하는 백엔드 스레드입니다. RHI(Render Hardware Interface) 계층에서 동작합니다. DX12와 Vulkan처럼 병렬 커맨드 제출을 지원하는 API에서는 RHI Thread가 독립적으로 동작하여 병렬 처리의 이점을 극대화합니다.
이 세 스레드는 파이프라인 방식으로 동작합니다. 예를 들어, Game Thread가 N+1 프레임을 처리하는 동안 Render Thread는 N 프레임의 렌더링 명령을 생성하고, RHI Thread는 N-1 프레임의 명령을 GPU에 전달합니다. 이러한 구조 때문에, 어느 한 스레드가 병목이 되면 나머지 스레드가 대기 상태에 빠지게 됩니다.
3. stat unit으로 병목 스레드 식별하기
본격적인 프로파일링에 앞서, 먼저 병목이 어디에 있는지 대략적으로 파악해야 합니다. 콘솔 명령 stat unit을 사용하면 각 스레드의 처리 시간을 실시간으로 확인할 수 있습니다.
게임 실행 중 콘솔 창(~ 키)을 열고 다음 명령을 입력합니다.
stat unit
위 명령을 실행하면 화면에 Frame, Game, Draw, GPU 네 가지 시간이 밀리초(ms) 단위로 표시됩니다. 각 항목의 의미는 다음과 같습니다.
- Frame: 한 프레임을 생성하는 데 걸린 총 시간입니다.
- Game: Game Thread에서 소모한 시간입니다.
- Draw: Render Thread에서 소모한 시간입니다.
- GPU: GPU가 씬을 렌더링하는 데 걸린 시간입니다.
병목 스레드를 판별하는 기준은 다음과 같습니다.
- Frame 시간이 Game 시간과 거의 같다면, Game Thread가 병목입니다.
- Frame 시간이 Draw 시간과 거의 같다면, Render Thread가 병목입니다.
- Game과 Draw 모두 낮은데 GPU만 높다면, GPU가 병목입니다.
stat unit은 빠르게 병목 지점의 방향을 잡아주는 도구입니다. 여기서 방향이 잡히면, 이제 Unreal Insights로 정밀 분석에 들어갑니다.
4. Unreal Insights 실행과 트레이스 녹화
4-1. Unreal Insights 실행 방법
Unreal Insights를 실행하는 방법은 두 가지입니다.
첫 번째 방법은 에디터 메뉴에서 Tools > Unreal Insights > Run Unreal Insights를 선택하는 것입니다.
두 번째 방법은 엔진 설치 경로의 Engine/Binaries/Win64/UnrealInsights.exe를 직접 실행하는 것입니다.
4-2. 커맨드라인에서 트레이스 채널 지정
트레이스 녹화는 게임 실행 시 커맨드라인 인자로 활성화할 수 있습니다. -trace 인자 뒤에 녹화할 채널을 쉼표로 구분하여 전달합니다.
일반적인 CPU/GPU 프로파일링에 적합한 채널 조합의 예시입니다.
# 상세한 분석이 필요한 경우 (오버헤드가 큼)
-trace=log,counters,cpu,frame,bookmark,file,loadtime,gpu,rhicommands,rendercommands,object
# 가벼운 프로파일링 (오버헤드가 적음)
-trace=counters,cpu,frame,bookmark,gpu
위 예시에서 첫 번째 옵션은 RHI 커맨드, 렌더 커맨드, 오브젝트 로딩까지 포함하여 가장 상세한 데이터를 수집합니다. 두 번째 옵션은 CPU와 GPU 핵심 데이터만 수집하여 게임 성능에 미치는 영향을 최소화합니다.
채널을 지정하지 않으면 기본적으로 cpu, frames, log, bookmark 채널이 활성화됩니다.
4-3. 런타임 중 트레이스 제어
게임이 이미 실행 중인 상태에서도 콘솔 명령으로 트레이스를 제어할 수 있습니다.
# 트레이스 녹화 시작
trace.start
# 트레이스 녹화 중지
trace.stop
위 명령으로 녹화를 중지하면, .utrace 파일이 Saved/Profiling/ 디렉토리에 저장됩니다. 이 파일을 Unreal Insights의 Session Browser에 드래그 앤 드롭하거나, Open Trace > Open File로 열 수 있습니다.
4-4. 추가 트레이스 옵션
더 정밀한 CPU 타이밍 이벤트가 필요한 경우, -statnamedevents 옵션을 -trace=cpu와 함께 사용하면 엔진 내부의 stat 이벤트까지 모두 추적할 수 있습니다.
스레드 간 컨텍스트 스위치를 확인하려면 -trace=default,contextswitch를 사용합니다. Task 시스템의 실행 경로를 추적하려면 -trace 목록에 task를 추가합니다.
# stat 이벤트 + 컨텍스트 스위치 + Task 추적
-trace=cpu,gpu,frame,bookmark,contextswitch,task -statnamedevents
위 조합을 사용하면 스레드별 실행 흐름, 컨텍스트 스위치, Task 시스템의 작업 분배까지 한 번에 확인할 수 있습니다.
5. Timing Insights로 스레드 병목 분석하기
Timing Insights는 Unreal Insights의 핵심 화면으로, 트레이스 세션의 프레임별 성능 데이터를 타임라인 형태로 표시합니다.
5-1. 타임라인 읽는 법
Timing Insights를 열면 상단에 프레임별 처리 시간 그래프가 표시되고, 하단에 각 스레드의 타임라인이 나타납니다. 각 스레드 트랙에는 해당 스레드에서 실행된 함수들이 블록 형태로 시각화됩니다.
스파이크가 발생한 프레임을 클릭하면, 해당 프레임에서 어떤 스레드의 어떤 함수가 시간을 많이 소모했는지 즉시 확인할 수 있습니다. 블록의 너비가 클수록 해당 함수의 실행 시간이 길다는 의미입니다.
5-2. Timers 패널과 Callers/Callees 분석
타임라인에서 특정 이벤트를 선택하면 하단의 Timers 패널에서 해당 이벤트의 상세 통계를 확인할 수 있습니다. 여기서 가장 중요한 지표는 다음 두 가지입니다.
- Inclusive Time(Inc Time): 해당 함수와 하위 함수 호출을 모두 포함한 총 실행 시간입니다.
- Exclusive Time(Exc Time): 해당 함수 자체만의 실행 시간입니다. 하위 호출 시간은 제외됩니다.
Callers/Callees 뷰를 통해 함수 호출 계층 구조를 탐색할 수 있습니다. 이 기능으로 병목의 근본 원인이 되는 함수를 정확하게 찾아낼 수 있습니다.
5-3. Main Graph로 이벤트 시각화
특정 이벤트를 타임라인에서 Shift + 클릭하면, Main Graph에 해당 이벤트가 플롯됩니다. 여러 이벤트를 동시에 플롯하면 시간 경과에 따른 처리 시간 변화를 비교할 수 있어서, 특정 상황에서 어떤 함수가 급격히 느려지는지 패턴을 파악하는 데 유용합니다.
5-4. CPU Stall 항목 해석 시 주의점
타임라인에서 CPU Stall이라는 항목을 볼 수 있습니다. 이것은 해당 스레드가 다른 스레드의 작업 완료를 기다리며 대기한 시간을 의미합니다. CPU Stall 자체는 해당 스레드의 성능 문제가 아니라, 다른 스레드가 병목이라는 신호입니다. 예를 들어 Render Thread에서 CPU Stall이 크게 나타난다면, Game Thread가 느려서 Render Thread가 기다리고 있는 상황일 수 있습니다.
6. C++ 코드에 프로파일링 마커 추가하기
Unreal Insights의 트레이스 데이터를 더 유용하게 만들려면, 프로젝트의 C++ 코드에 직접 프로파일링 마커를 삽입하는 것이 중요합니다. 마커가 없으면 엔진 내부 함수만 보이기 때문에, 자신의 코드 중 어떤 부분이 느린지 파악하기 어렵습니다.
6-1. TRACE_CPUPROFILER_EVENT_SCOPE
가장 기본적인 CPU 프로파일링 매크로입니다. 스코프 기반으로 동작하며, 해당 스코프의 시작과 끝 시점을 자동으로 기록합니다.
다음은 자신의 함수에 프로파일링 마커를 추가하는 예시입니다.
void AMyCharacter::PerformHeavyCalculation()
{
// Unreal Insights 타임라인에 "AMyCharacter::PerformHeavyCalculation"으로 표시
TRACE_CPUPROFILER_EVENT_SCOPE(AMyCharacter::PerformHeavyCalculation);
for (int32 Index = 0; Index < LargeDataSet.Num(); ++Index)
{
ProcessSingleItem(LargeDataSet[Index]);
}
}
위 코드처럼 TRACE_CPUPROFILER_EVENT_SCOPE 매크로를 함수 시작 부분에 배치하면, Unreal Insights의 타임라인에서 해당 함수의 실행 시간을 확인할 수 있습니다. 매크로의 인자로 전달한 이름이 그대로 타임라인에 표시됩니다.
6-2. TRACE_BOOKMARK
특정 시점에 북마크를 남기고 싶을 때 사용합니다. 타임라인에 수직 마커로 표시되어, 특정 이벤트가 발생한 시점을 쉽게 찾을 수 있습니다.
다음은 게임플레이 이벤트 발생 시 북마크를 남기는 예시입니다.
void AMyGameMode::OnWaveStarted(int32 WaveNumber)
{
// 타임라인에 북마크로 표시되어 해당 시점을 빠르게 찾을 수 있음
TRACE_BOOKMARK(TEXT("Wave %d Started"), WaveNumber);
SpawnEnemiesForWave(WaveNumber);
}
위 코드에서 TRACE_BOOKMARK는 Unreal Insights 타임라인에 표식을 남깁니다. 예를 들어 "Wave 3에서 프레임 드롭이 발생한다"는 보고를 받았을 때, 북마크를 통해 해당 시점으로 바로 이동하여 분석을 시작할 수 있습니다.
6-3. UE_LOG와 함께 활용하기
UE_LOG로 출력한 로그도 Unreal Insights의 Log 패널에 표시됩니다. 따라서 분석하고 싶은 코드 영역 근처에 로그를 남겨두면, 타임라인에서 시간 범위를 선택할 때 해당 구간의 로그를 함께 확인할 수 있습니다.
다음은 성능에 영향을 줄 수 있는 로직에 로그와 프로파일링 마커를 함께 사용하는 예시입니다.
void AMyAIController::RunPathfinding()
{
TRACE_CPUPROFILER_EVENT_SCOPE(AMyAIController::RunPathfinding);
UE_LOG(LogAI, Verbose, TEXT("Pathfinding started for %s"), *GetName());
FNavPathSharedPtr ResultPath;
const bool bSuccess = FindPathToTarget(TargetLocation, ResultPath);
if (!bSuccess)
{
UE_LOG(LogAI, Warning, TEXT("Pathfinding failed for %s"), *GetName());
}
}
위 코드처럼 TRACE_CPUPROFILER_EVENT_SCOPE와 UE_LOG를 함께 사용하면, 타임라인에서 처리 시간을 확인하면서 동시에 로그 패널에서 관련 로그를 필터링하여 볼 수 있습니다.
7. 스레드별 최적화 팁
7-1. Game Thread 최적화
Game Thread가 병목인 경우 다음 사항들을 점검합니다.
Tick 최적화: 매 프레임 Tick이 필요하지 않은 액터는 SetActorTickInterval을 사용하여 Tick 간격을 늘리거나, SetActorTickEnabled(false)로 Tick을 비활성화합니다. PrimaryActorTick.bCanEverTick = false를 기본값으로 설정하고, 필요한 액터만 Tick을 활성화하는 습관이 중요합니다.
다음은 Tick 간격을 조절하는 예시입니다.
AMyEnvironmentActor::AMyEnvironmentActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickInterval = 0.5f; // 0.5초마다 한 번씩 Tick
}
위 코드처럼 TickInterval을 설정하면 불필요한 매 프레임 Tick을 줄여 Game Thread 부하를 낮출 수 있습니다.
타이머 활용: 주기적인 체크 로직은 Tick 대신 FTimerManager를 사용하는 것이 효율적입니다.
비동기 처리: 무거운 연산(경로 탐색, 대량 데이터 처리 등)은 AsyncTask나 FRunnable을 통해 워커 스레드에서 실행합니다.
7-2. Render Thread 최적화
Render Thread가 병목인 경우 드로우 콜 수를 점검합니다.
드로우 콜 줄이기: stat rhi 명령으로 드로우 콜 수를 확인하고, 메시 병합(Merge Actors 기능), 인스턴싱(Instanced Static Mesh), LOD 설정 등을 활용하여 줄입니다.
오클루전 컬링 확인: 화면에 보이지 않는 오브젝트가 렌더링되고 있지 않은지 확인합니다. stat initviews 명령으로 가시성 판정에 걸리는 시간을 확인할 수 있습니다.
7-3. GPU 최적화
GPU가 병목인 경우, stat gpu에서 가장 시간이 큰 패스를 확인합니다.
셰도우 최적화: Shadow Depths 비용이 높다면, 다이내믹 셰도우를 사용하는 라이트 수를 줄이거나, 셰도우 캐스케이드 수와 해상도를 조절합니다.
포스트 프로세싱 점검: Post Processing 비용이 높다면, 불필요한 포스트 프로세스 이펙트를 비활성화하거나 품질을 낮춥니다.
오버드로우 확인: 반투명 오브젝트가 많으면 오버드로우가 증가합니다. 에디터의 Optimization Viewmodes > Shader Complexity를 통해 오버드로우 상태를 시각적으로 확인할 수 있습니다.
8. 일반 C++ 프로파일링 도구와의 비교
일반 C++ 개발에서도 프로파일링을 위한 다양한 도구가 존재합니다. Visual Studio의 Performance Profiler, Intel VTune, Valgrind(Linux) 등이 대표적입니다. 이 도구들과 Unreal Insights의 공통점과 차이점을 살펴봅니다.
공통점: 함수 단위의 실행 시간 측정, 호출 계층(Call Stack) 분석, 샘플링 기반 프로파일링 등 기본적인 접근 방식은 동일합니다. 또한 코드에 직접 마커를 삽입하여 관심 영역을 표시하는 방식도 유사합니다.
차이점: 일반 C++ 프로파일러는 순수 CPU 실행 시간만 측정하는 데 반해, Unreal Insights는 엔진 특유의 Game Thread, Render Thread, RHI Thread, GPU의 파이프라인 구조를 인식하고 각 스레드를 별도 트랙으로 시각화합니다. 또한 TRACE_CPUPROFILER_EVENT_SCOPE 같은 UE 전용 매크로는 엔진의 트레이스 시스템과 통합되어, 프레임 경계나 렌더링 패스와 같은 엔진 고유의 컨텍스트 안에서 성능 데이터를 해석할 수 있게 해줍니다. 일반 프로파일러에서는 이러한 엔진 수준의 맥락을 자동으로 제공하지 않습니다.
일반 C++ 프로파일러로 스코프 기반 측정을 구현하려면 직접 RAII 패턴의 타이머 클래스를 작성해야 합니다.
// 일반 C++: 스코프 기반 타이머를 직접 구현
class ScopedTimer
{
public:
ScopedTimer(const char* InName)
: Name(InName)
, StartTime(std::chrono::high_resolution_clock::now())
{}
~ScopedTimer()
{
auto EndTime = std::chrono::high_resolution_clock::now();
auto Duration = std::chrono::duration_cast<std::chrono::microseconds>(EndTime - StartTime);
printf("[Profile] %s: %lld us\n", Name, Duration.count());
}
private:
const char* Name;
std::chrono::time_point<std::chrono::high_resolution_clock> StartTime;
};
// 사용법
void HeavyFunction()
{
ScopedTimer Timer("HeavyFunction");
// ... 로직
}
위의 일반 C++ 방식은 단순한 시간 측정만 가능하고, 결과를 콘솔에 출력하는 정도에 그칩니다.
// 언리얼 엔진: 한 줄로 트레이스 시스템과 통합
void AMyActor::HeavyFunction()
{
TRACE_CPUPROFILER_EVENT_SCOPE(AMyActor::HeavyFunction);
// ... 로직
}
위 코드처럼 언리얼 엔진에서는 TRACE_CPUPROFILER_EVENT_SCOPE 매크로 한 줄만으로, 결과가 Unreal Insights의 타임라인, Timers 패널, Callers/Callees 뷰에 모두 자동으로 통합됩니다.
9. 유니티 엔진과의 비교
유니티 엔진에도 성능 프로파일링을 위한 Unity Profiler가 내장되어 있습니다.
유사점: 두 도구 모두 프레임 단위로 CPU와 GPU의 처리 시간을 측정하고, 함수 호출 계층을 분석할 수 있습니다. 또한 사용자 정의 마커를 삽입하여 특정 코드 영역을 추적하는 기능을 제공합니다.
구조적 차이: Unity Profiler는 에디터에 내장된 형태로, 에디터 실행 중에 실시간으로 프로파일링합니다. 반면 Unreal Insights는 독립 실행형 애플리케이션으로, 트레이스 데이터를 파일로 저장한 뒤 별도로 분석합니다. 이 방식은 프로파일링 도구 자체가 게임 성능에 미치는 영향을 최소화할 수 있다는 장점이 있습니다.
스레드 분석 깊이: 유니티는 Main Thread와 Render Thread 정도로 구분하는 반면, Unreal Insights는 Game Thread, Render Thread, RHI Thread, 각종 워커 스레드, Task 시스템까지 개별 트랙으로 시각화합니다. 특히 컨텍스트 스위치와 Task 실행 경로를 추적할 수 있어서, 복잡한 멀티스레드 환경에서의 병목 분석에 더 깊은 수준의 데이터를 제공합니다.
언리얼 엔진만의 강점: Unreal Insights는 트레이스 채널 시스템을 통해 필요한 데이터만 선택적으로 수집할 수 있어서, 프로파일링 오버헤드를 정밀하게 제어할 수 있습니다. 또한 .utrace 파일로 저장된 세션을 팀원과 공유하여 동일한 데이터를 함께 분석할 수 있다는 점도 협업 환경에서 큰 이점입니다.
10. 주의사항
10-1. 디버그 빌드에서 프로파일링하지 않기
Debug 빌드는 최적화가 비활성화되어 있어서 실제 성능과 크게 다른 결과를 보여줍니다. 프로파일링은 반드시 Development 또는 Test 빌드에서 수행해야 합니다. Shipping 빌드는 대부분의 stat 명령과 프로파일링 코드가 제거되어 있으므로, Development 빌드가 가장 적합합니다.
10-2. 충분한 샘플 확보하기
프레임 하나의 스냅샷만으로 성능을 판단하면 안 됩니다. 게임의 다양한 상황(전투 장면, 오픈 월드 탐색, UI가 많은 장면 등)에서 충분한 시간 동안 데이터를 수집해야 합니다. 특정 구간에서만 발생하는 스파이크는 짧은 캡처로는 놓칠 수 있습니다.
10-3. 에디터 오버헤드 인식하기
에디터 내에서 Play-In-Editor(PIE)로 실행하면 에디터 자체의 오버헤드가 포함됩니다. 정확한 성능 측정을 위해서는 Standalone 모드로 실행하거나, 패키징된 빌드에서 측정하는 것이 바람직합니다.
10-4. stat unit만으로 결론 내리지 않기
stat unit은 병목의 방향을 잡아주는 도구일 뿐, 근본 원인을 알려주지는 않습니다. 반드시 Unreal Insights나 상세 stat 명령을 통해 구체적인 원인을 분석해야 합니다.
10-5. 프로파일링 마커의 오버헤드
TRACE_CPUPROFILER_EVENT_SCOPE는 매우 가벼운 매크로이지만, 초당 수만 번 호출되는 함수에 삽입하면 트레이싱 자체의 오버헤드가 측정 결과를 왜곡할 수 있습니다. 이런 경우에는 상위 스코프에만 마커를 배치하거나, 필요한 순간에만 트레이스를 활성화하는 것이 좋습니다.
10-6. CPU Stall을 성능 문제로 오해하지 않기
앞서 설명한 것처럼, CPU Stall은 해당 스레드가 대기 중이라는 의미이지, 그 스레드 자체가 느리다는 의미가 아닙니다. CPU Stall이 큰 스레드가 아니라, CPU Stall을 유발하는 다른 스레드를 최적화해야 합니다.
Unreal Insights는 언리얼 엔진 5에서 성능 분석의 중심이 되는 도구입니다. stat unit으로 병목 방향을 잡고, Unreal Insights의 Timing Insights로 스레드별 함수 단위까지 파고들어 근본 원인을 찾는 워크플로우를 익히면, 프로젝트의 성능 문제를 체계적으로 해결할 수 있습니다. 프로파일링 마커를 습관적으로 코드에 삽입하여, 문제가 발생했을 때 즉시 원인을 추적할 수 있는 환경을 미리 구축해두는 것을 권장합니다.
'언리얼 엔진 5' 카테고리의 다른 글
| [UE5] Render Thread 최적화 (0) | 2026.03.05 |
|---|---|
| [UE5] Game Thread 최적화 (0) | 2026.03.04 |
| [UE5] 프로파일링 - stat 명령어로 스레드 병목 진단하기 (1) | 2026.03.02 |
| [UE5] 게임플레이 프레임워크 구조 이해하기 (0) | 2026.03.02 |
| [UE5] 멀티스레딩 (0) | 2026.03.01 |