Youngki Jeong | Game Dev Lab
HomeProjectsEvidenceJourneyAbout
SearchGitHub글쓰기

Youngki Jeong | Game Dev Lab

Unreal C++ 게임개발 포트폴리오 & 개발 기록

Projects·Evidence·Journey·About·GitHub Portfolio·GitHub

빠른 검색

프로젝트, Evidence, 디버깅 노트, 학습 기록을 검색합니다.

← 목록으로
← 목록으로
RIT-B08

Performance를 측정과 검증의 문제로 보는 이유

2026년 9월 7일
·
JEONGYOUNGKI
Description

Performance Candidate와 Measured Bottleneck을 구분하고 Baseline과 Before/After 측정을 중심으로 성능 검증 기준을 정리한 글입니다.

Project

RiteSeekers

ArticleType

Performance

SourceBoundary

Diagnostic Guide

EvidenceLevel

Verification Guide

Priority

B

Tags

Performance

ProofType

Source

Audience

Technical Interviewer

CareerTrack

Gameplay

목차

RIT-B08 | RiteSeekers — 성능 문제를 코드보다 측정에서 먼저 보기 시작한 과정

RiteSeekers에서는 처음부터 성능 최적화를 깊게 다루지는 않았습니다.

먼저 Experience, Ability, Combat, Inventory, Equipment처럼 여러 Gameplay 시스템이 실제로 어떻게 이어지는지를 이해하는 데 더 많은 시간을 썼습니다.

그런데 Source를 계속 보다 보니 자연스럽게 신경 쓰이는 코드들이 생겼습니다.

Text
매 Frame 실행되는가
 
몇 개의 Actor / Component가 같은 일을 하는가
 
한 번 실행할 때 얼마나 많은 일을 하는가

예전에는 Tick처럼 자주 실행되는 코드를 발견하면 먼저 “이걸 줄여야 하나?”라고 생각했습니다.

지금은 조금 다르게 봅니다.

코드가 무거워 보인다는 이유만으로 바로 수정하기보다, 실제 Runtime에서 이 부분이 비용으로 잡히는지 먼저 확인하는 편이 맞다고 생각합니다.

특정 함수를 최적화해서 몇 ms를 줄였다는 결과보다, Source에서 의심되는 부분을 발견했을 때 실제 Runtime에서 어디부터 확인할지 기준을 잡아본 과정에 가깝습니다.


무거워 보이는 코드와 실제 Bottleneck은 달랐다

Source를 읽다 보면 눈에 먼저 들어오는 코드가 있습니다.

Tick, UI Binding, 반복되는 Spawn / Destroy 같은 부분입니다.

이런 코드는 비용이 생길 가능성은 있습니다.

하지만 실제 Frame을 느리게 만드는 원인이 반드시 그곳이라고 볼 수는 없습니다.

Text
Game Thread
 
Render Thread
 
GPU
 
UI
 
Animation / Asset
 
Object 생성과 제거

실제 Frame Time이 어디에서 사용되고 있는지는 측정하기 전에는 알기 어렵습니다.

그래서 Source에서 발견한 코드를 바로 Bottleneck이라고 부르지 않게 됐습니다.

제가 구분해서 보는 기준은 이 정도입니다.

Text
Performance Candidate
→ 비용을 확인해볼 만한 부분
 
Measured Bottleneck
→ Profiler에서 실제 비용이 확인된 부분
 
Optimization Result
→ 수정 후 같은 조건에서 다시 측정해 개선된 부분

Candidate에서 Bottleneck으로 넘어가려면 실제 Runtime 측정이 필요합니다.

이 기준을 두고 나니 “코드가 무거워 보인다”는 이유만으로 먼저 구조를 바꾸는 일을 줄일 수 있었습니다.


처음에는 Frame의 큰 방향부터 본다

성능 문제가 느껴진다면 특정 C++ 함수부터 열어보기보다 먼저 Frame의 어느 쪽에서 시간이 많이 쓰이는지 보는 편이 낫다고 생각했습니다.

RiteSeekers에서는 먼저 stat unit으로 Frame의 큰 방향을 확인했습니다.

Text
Frame
Game
Draw
GPU

예를 들어 Game 시간이 상대적으로 크게 나온다면 Game Thread에서 실행되는 Gameplay 쪽 비용을 더 확인해볼 이유가 생깁니다.

반대로 GPU 쪽이 높다면 Gameplay Tick부터 고치는 것은 실제 문제와 관계없는 작업일 수 있습니다.

그래서 첫 확인은 이 정도로 생각하고 있습니다.

Text
같은 상황 재현
→ stat unit
→ Frame / Game / Draw / GPU 비교
→ 다음에 볼 영역 결정

stat unit만으로 어느 함수가 문제인지까지 알 수 있는 것은 아닙니다.

어느 쪽을 더 확인할지 정하기 위한 첫 Baseline 정도로 보고 있습니다.

RIT-B08_stat_unit_Baseline.png

📸 Screenshot 1-B. 같은 Baseline에서 Frame 10.03 ms, Game 4.87 ms, Draw 4.18 ms, GPU 5.12 ms를 확인한 측정 결과.

RIT-B08_stat_unit_Result.png

📸 Screenshot 1-B. 같은 Baseline에서 Frame 10.03 ms, Game 4.87 ms, Draw 4.18 ms, GPU 5.12 ms를 확인한 측정 결과.

이번 Baseline에서는 Game 4.87 ms, Draw 4.18 ms, GPU 5.12 ms로 한쪽만 크게 튀는 모습은 보이지 않았습니다. 그래서 Source에서 Tick이 보인다는 이유만으로 바로 수정하지 않고, 실제 성능 문제가 재현될 때 필요한 영역을 더 좁혀서 확인하는 쪽으로 두었습니다.
화면에는 RHIT도 함께 표시됐지만, 첫 Baseline에서는 특정 세부 항목보다 Frame / Game / Draw / GPU의 큰 방향을 먼저 봤습니다.


Tick은 있다는 이유만으로 없애지 않았다

Unreal에서 Tick은 필요한 기능입니다.

매 Frame 상태를 확인해야 하는 기능이라면 Tick을 쓰는 것 자체가 이상한 건 아닙니다.

그래서 Tick을 발견했을 때는 먼저 세 가지를 생각합니다.

Text
Frequency
→ 얼마나 자주 실행되는가
 
Population
→ 몇 개의 Actor / Component가 실행하는가
 
Work
→ 한 번 실행할 때 얼마나 많은 일을 하는가

가벼운 Tick도 수백 개의 Object에서 매 Frame 실행된다면 비용이 커질 수 있습니다.

반대로 몇 개 되지 않는 Object에서 작은 연산만 한다면 Event 구조로 바꾸는 것이 오히려 코드만 복잡하게 만들 수도 있습니다.

Source를 볼 때는 이런 정도를 같이 봅니다.

Text
매 Frame 필요한 작업인가
 
상태가 바뀔 때만 처리해도 되는가
 
Timer 정도면 충분한가
 
Tick Interval을 조절할 수 있는가
 
필요한 동안만 Tick을 켤 수 있는가

예를 들어 Equipment 변경이나 AbilitySet Grant처럼 변화 시점이 명확한 작업이라면 매 Frame 확인하는 것보다 변경될 때 처리하는 방식이 더 자연스러울 수 있습니다.

그렇다고 Event 방식이라는 이유만으로 무조건 더 빠르다고 생각하지는 않습니다.

실제 비용이 거의 없다면 굳이 구조를 바꿀 이유도 없습니다.


Source에서 눈에 들어온 반복 실행 위치

RiteSeekers Source에서도 반복 실행되는 위치가 있었습니다.

현재 확인한 예 중 하나는:

Text
URSElectricFieldManagerComponent::TickComponent

입니다.

또 URSGameplayAbility_Sprint_Active::OnSprintCommitTick도 이름과 Source 흐름을 보면 실제 호출 빈도와 비용을 한 번 확인해볼 수 있는 위치입니다.

하지만 지금 이 글에서:

Text
ElectricField가 성능 Bottleneck이다
 
Sprint가 느리다

라고 말할 수는 없습니다.

아직 Unreal Insights 같은 Profiler에서 해당 함수 자체의 실행 비용을 직접 확인한 것은 아니기 때문입니다.

현재 말할 수 있는 건 이 정도입니다.

Text
Source에서 반복 실행 지점을 발견했다
→ 실제 비용을 확인해볼 Candidate다

이런 코드를 발견했다고 바로 “최적화 대상”이라고 부르지 않게 된 것도 예전과 달라진 부분입니다.


UI도 반복되고 있는지부터 확인한다

UI도 반복 실행 비용이 생길 수 있는 영역입니다.

HP Bar, Equipment Icon, Skill Icon 자체는 단순해 보여도 Widget이 계속 값을 읽고 계산한다면 비용이 쌓일 수 있습니다.

Health UI를 예로 들면 두 가지 방식으로 생각할 수 있습니다.

Text
매번 Health를 읽는다
→ 현재 값을 다시 계산

또는:

Text
HealthChanged
→ Health가 실제로 변경됨
→ UI 갱신

입니다.

Equipment UI도 실제 Equipment State가 바뀌는 순간에 Icon이나 Tooltip을 갱신할 수 있다면 계속 같은 값을 확인할 필요가 없을 수도 있습니다.

하지만 Binding이 있다는 사실만으로 UI Bottleneck이라고 부를 수는 없습니다.

Widget 수가 적거나 Binding에서 하는 일이 작다면 실제 Frame에 미치는 영향은 거의 없을 수도 있습니다.

그래서 UI 쪽 비용이 실제로 의심될 때는 Slate Insights나 stat slate 같은 도구로 확인하는 편이 맞다고 보고 있습니다.

현재 RiteSeekers의 모든 Widget이 Event / Delegate 기반이라고 설명하지도 않습니다.

실제 Widget Blueprint Wiring과 Runtime 비용을 확인하기 전까지는 그 정도로 강하게 말하기 어렵습니다.


Gameplay State와 UI 갱신도 나눠서 봤다

성능을 생각하면서 UI의 책임도 같이 보게 됐습니다.

Health라면:

Text
Gameplay
→ Health 변경
 
UI
→ 변경된 Health 표시

Equipment라면:

Text
Equipment State 변경
→ Equipment UI 갱신

처럼 볼 수 있습니다.

Gameplay State를 계산하는 부분과 화면에 보여주는 부분이 섞이면 구조를 따라가기 어려워지고, 같은 값을 여러 곳에서 반복해서 계산할 가능성도 생깁니다.

그래서 값이 실제로 바뀌는 시점을 알 수 있다면 그 결과를 UI가 받아서 표시하는 방식도 먼저 생각해보게 됐습니다.

이것 역시 “Event 방식으로 바꿨더니 성능이 좋아졌다”는 결과는 아닙니다.

UI 비용을 실제로 측정해 개선한 것은 아니고, 비용이 의심될 때 먼저 확인해볼 구조 중 하나로 보고 있습니다.


Spawn / Destroy가 보인다고 바로 Pooling을 넣지는 않는다

전투 게임에서는 Object가 반복해서 만들어지고 사라지는 상황이 생길 수 있습니다.

예를 들면:

Text
Projectile
Effect Actor
Damage Number
Pickup

같은 것들입니다.

높은 빈도로 Spawn / Destroy가 반복된다면 Actor 생성, Component 초기화와 등록, 제거 비용을 확인해볼 필요가 있습니다.

이럴 때 바로 생각나는 방법 중 하나가 Object Pooling입니다.

하지만 Pooling도 공짜는 아닙니다.

Text
Object State Reset
 
Delegate / Timer 정리
 
Effect 초기화
 
Lifetime 관리
 
Replication / Ownership 처리
 
Pool Memory 유지

같이 새로 관리해야 할 부분이 생깁니다.

그래서:

Text
Spawn / Destroy가 있다
→ Pooling 적용

처럼 바로 이어서 생각하지는 않습니다.

실제로 반복 생성이 얼마나 많은지, Profiler에서 의미 있는 비용으로 잡히는지 보고 난 뒤 Pooling의 복잡도를 감수할 가치가 있는지를 판단하는 편이 낫다고 생각합니다.

위에 적은 모든 Object가 현재 RiteSeekers에서 실제로 높은 빈도로 Spawn / Destroy되고 있다는 의미도 아닙니다.

그 부분은 실제 Source와 Runtime을 보고 판단해야 합니다.


도구마다 보는 위치가 달랐다

성능을 확인할 때 사용하는 도구도 목적이 조금씩 달랐습니다.

Text
stat unit
→ Frame / Game / Draw / GPU
→ 큰 방향 확인
 
stat game
→ Game Thread의 주요 Runtime 비용 확인
 
Unreal Insights / Timing Insights
→ CPU Timeline
→ 함수 / Event 구간
→ 호출 횟수와 시간 확인
 
Slate Insights / stat slate
→ Slate / UMG 비용 확인
RIT-B08_stat_game_Command.png

📸 Screenshot 2. stat unit으로 큰 방향을 확인한 뒤 Game Thread 내부 비용을 더 나눠 보기 위해 stat game을 실행한 화면.

RIT-B08_stat_game_GameThread_Breakdown.png

📸 Screenshot 3. stat game에서 World Tick, Tick, Blueprint 등 Game Thread 내부의 주요 Runtime 비용을 항목별로 확인한 화면.

stat game에서는 Game Thread 안에서 시간이 어떤 항목에 분포하는지 확인했습니다. 여기에서도 특정 항목이 보인다는 이유만으로 바로 Bottleneck이라고 판단하지 않았고, 특정 함수의 호출 시간까지 확인할 필요가 생기면 그때 Unreal Insights로 더 자세히 보는 순서로 잡았습니다.

Gameplay Debugger는 조금 다른 용도였습니다.

ASC, GameplayTag, Ability State를 보는 데는 도움이 되지만 Frame Time이나 함수 실행 시간을 측정하는 Profiler는 아닙니다.

예를 들어 특정 Ability가 왜 자주 실행되는지는 Gameplay Debugger나 Runtime State를 보면서 이해할 수 있습니다.

하지만 그 Ability가 실제 Frame Time을 얼마나 쓰는지는 Stats나 Unreal Insights 같은 Performance 도구로 확인해야 합니다.


측정에서 비용이 보인 뒤 Source를 다시 본다

예전에는 Source를 읽다가 이런 생각을 하는 경우가 있었습니다.

Text
이 함수가 많이 호출되는 것 같다
→ 아마 느릴 것이다

지금은 가능하면 반대 순서로 봅니다.

Text
성능 문제 재현
→ stat unit으로 큰 방향 확인
→ stat game으로 Game Thread 내부 확인
→ 필요한 경우 Profiler로 함수 / Event 확인
→ Source / Editor 설정 확인

예를 들어 Unreal Insights에서 특정 Component Tick이 실제로 비용을 쓰고 있다면 그때 Source를 열어 왜 반복되는지 확인합니다.

UI가 비용으로 보이면 그때 Widget Blueprint의 Binding이나 Update 구조를 따라가면 됩니다.

Profiler에서 실제 문제가 보이지도 않았는데 Tick Source Screenshot부터 꺼내서 “최적화 대상”이라고 설명하는 건 오히려 근거가 약하다고 생각합니다.


수정했다면 같은 조건으로 다시 봐야 한다

Profiler에서 실제 비용을 확인하고 코드를 수정했다면 그다음에는 변경 전과 변경 후를 다시 비교해야 합니다.

Text
Baseline
→ Bottleneck 측정
→ 변경
→ 같은 조건 재측정
→ Before / After 비교

여기서 중요한 건 같은 조건이라고 생각합니다.

가능하면 다음 조건을 맞춰야 비교하기 편합니다.

Text
같은 Map / 전투 상황
같은 Build Configuration
같은 해상도 / Scalability
같은 Character / Enemy 수
같은 입력 시나리오
같은 Capture 시간
가능하면 비슷한 Warm-up 상태

FPS 하나만 보는 것으로 끝내는 것도 부족합니다.

Text
Before
→ Frame / Game / Draw / GPU
→ 실제 문제 구간 비용
 
Change
→ 무엇을 왜 바꿨는가
 
After
→ 같은 조건에서 다시 측정
 
Result
→ 실제 비용이 줄었는가

정도까지 있어야 우연히 Frame이 달라진 것과 실제 개선을 구분하기 쉬워집니다.


지금 RiteSeekers에서 말할 수 있는 범위

현재 RiteSeekers에서 성능과 관련해 말할 수 있는 범위는 실제 최적화 성과보다 측정 기준을 잡은 단계입니다.

Source에서는 Tick이나 UI, Spawn / Destroy처럼 한 번 확인해볼 만한 위치를 찾을 수 있습니다.

실제로 Performance 문제가 보인다면:

Text
stat unit
→ Frame의 큰 방향 확인
 
stat game
→ Game Thread 내부 항목 확인
 
필요한 Profiler
→ 함수 / Event 단위 비용 확인
 
Source / Editor
→ 원인 확인
 
Before / After
→ 변경 결과 확인

순서로 내려가려고 합니다.

반대로 지금은 다음 같은 이야기를 하지 않습니다.

Text
특정 함수가 실제 Bottleneck이었다
 
몇 ms를 줄였다
 
FPS를 몇 % 개선했다
 
Object Pooling으로 성능을 개선했다

이런 결과를 말하려면 실제 Baseline과 Profiler 결과, 같은 조건에서 찍은 Before / After가 있어야 하기 때문입니다.

없는 수치를 채워 넣는 것보다 지금 실제로 확인한 범위를 그대로 남기는 쪽이 더 낫다고 생각했습니다.


프로젝트를 진행하며 달라진 부분

예전에는 최적화를:

Text
느려 보이는 코드를
더 빠르게 바꾸는 일

이라고 생각했습니다.

지금은 그 전에 확인할 게 더 많다고 생각합니다.

Text
실제 문제가 있는지 확인
→ 어느 영역이 느린지 측정
→ 실제 비용이 있는 위치 확인
→ 필요한 부분만 변경
→ 같은 조건에서 다시 측정

Tick을 없앴다거나 Pooling을 넣었다는 사실 자체보다, 왜 그 코드를 바꿔야 했는지 실제 측정 결과로 설명할 수 있는가가 더 중요해졌습니다.

반대로 Profiler에서 의미 있는 비용이 보이지 않는다면 구조를 그대로 두는 것도 하나의 판단이라고 생각합니다.


정리

RiteSeekers Source를 다시 보면서 성능에 대해 가장 크게 바뀐 생각은 비용이 커 보이는 코드와 실제 Bottleneck은 다를 수 있다는 점이었습니다.

지금 제가 생각하는 흐름은 이렇습니다.

Text
문제 재현
→ stat unit
→ stat game
→ 필요한 경우 Profiler
→ 실제 비용 확인
→ Source / Editor 확인
→ 필요한 부분만 변경
→ 동일 조건 재측정

Tick, UI Binding, Spawn / Destroy도 존재한다는 이유만으로 문제라고 보지는 않습니다.

마찬가지로 Profiler를 한 번 열어봤다는 사실만으로 성능을 개선했다고 말할 수도 없습니다.

지금 남기고 싶은 것은 “RiteSeekers의 성능을 최적화했다”는 결과보다, Source에서 의심되는 부분을 발견했을 때 바로 결론을 내리지 않고 실제 측정 결과를 보고 다음 확인 위치를 정한 과정입니다.

빠른 검색

프로젝트, Evidence, 디버깅 노트, 학습 기록을 검색합니다.

목차