콘텐츠로 이동

Unity Profiler CPU/GPU 분석

Unity 프로파일러는 CPU·GPU·메모리·렌더링 병목을 찾는 1차 진단 도구입니다. “느리다”는 현상에서 “왜 느린가”로 이동하려면 프로파일러 데이터를 체계적으로 읽는 방법을 알아야 합니다.


Window > Analysis > Profiler (단축키 Ctrl+7)

Deep Profile 모드: 모든 C# 메서드 호출을 계측합니다. 오버헤드가 크므로 의심 영역을 좁힌 후 사용합니다.

코드에서 직접 마커를 삽입하면 프로파일러 타임라인에 구간이 표시됩니다.

using Unity.Profiling;
public class EnemyManager : MonoBehaviour
{
// static으로 선언해 매 프레임 할당을 피함
private static readonly ProfilerMarker s_UpdateMarker =
new ProfilerMarker(ProfilerCategory.Scripts, "EnemyManager.UpdateAll");
private static readonly ProfilerMarker s_PathfindMarker =
new ProfilerMarker(ProfilerCategory.AI, "EnemyManager.Pathfind");
void Update()
{
using (s_UpdateMarker.Auto())
{
foreach (var enemy in _activeEnemies)
{
using (s_PathfindMarker.Auto())
enemy.UpdatePath();
enemy.UpdateAnimation();
}
}
}
}

ProfilerMarkerDevelopment Build에서만 활성화되며 Release 빌드에서는 zero-cost로 제거됩니다.


// 프로파일러에서 이런 패턴이 보이면:
// MainThread: [Physics.Step 18ms] [Render.OpaqueGeometry 4ms]
// → Physics가 CPU 예산을 독점
// 해결: Physics.simulationMode를 Script로 전환하고 분산 실행
void FixedUpdate()
{
// 무거운 Physics는 멀티 스텝으로 분할
Physics.Simulate(Time.fixedDeltaTime * 0.5f);
// 중간에 다른 로직 처리
Physics.Simulate(Time.fixedDeltaTime * 0.5f);
}

프로파일러 Memory 트랙에서 GC.Alloc이 빈번하게 보이면:

// 나쁜 예: 매 프레임 문자열 생성
void Update()
{
debugText.text = "Score: " + score.ToString(); // 박싱 + 문자열 연결
}
// 좋은 예: StringBuilder 재사용
private readonly System.Text.StringBuilder _sb = new(64);
void Update()
{
_sb.Clear();
_sb.Append("Score: ");
_sb.Append(score);
debugText.text = _sb.ToString();
}

Window > Analysis > Frame Debugger

Frame Debugger는 각 드로우콜을 순서대로 재생하며 어떤 오브젝트가 얼마나 많은 드로우콜을 유발하는지 보여줍니다.

확인 포인트:

  • Shadow Caster 드로우콜이 렌더링 드로우콜의 2배인 경우 → Cast ShadowsOff로 설정
  • 동일 메시가 여러 번 그려지는 경우 → GPU Instancing 또는 Static Batching 검토
// Material 설정에서 Enable GPU Instancing 체크 후
// 코드에서 MaterialPropertyBlock 사용
public class GrassRenderer : MonoBehaviour
{
[SerializeField] private Mesh grassMesh;
[SerializeField] private Material grassMaterial;
private Matrix4x4[] _matrices;
private MaterialPropertyBlock _mpb;
void Start()
{
_matrices = new Matrix4x4[1023]; // DrawMeshInstanced 최대 1023
_mpb = new MaterialPropertyBlock();
for (int i = 0; i < _matrices.Length; i++)
{
_matrices[i] = Matrix4x4.TRS(
new Vector3(Random.Range(-50f, 50f), 0, Random.Range(-50f, 50f)),
Quaternion.identity,
Vector3.one
);
}
}
void Update()
{
Graphics.DrawMeshInstanced(grassMesh, 0, grassMaterial, _matrices,
_matrices.Length, _mpb);
}
}

Stats 패널 (Game 뷰 우상단 "Stats" 버튼)
항목설명권장 기준 (모바일)
Batches드로우콜 수< 100
Tris삼각형 수< 300K
Verts정점 수< 150K
SetPass Calls셰이더 패스 전환< 50
// 런타임에서 렌더링 통계 수집
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
void LogRenderStats()
{
// URP DebugManager 활용
var debugManager = DebugManager.instance;
// 또는 Unity Statistics API
Debug.Log($"Draw Calls: {UnityStats.drawCalls}");
Debug.Log($"Triangles: {UnityStats.triangles}");
}

Window > Analysis > Memory Profiler (패키지 설치 필요)

// 스냅샷 코드에서 수동 요청
using Unity.Profiling.Memory;
public class MemoryDebugger : MonoBehaviour
{
[ContextMenu("Take Memory Snapshot")]
void TakeSnapshot()
{
MemoryProfiler.TakeSnapshot("snapshot", OnSnapshotFinished,
CaptureFlags.ManagedObjects | CaptureFlags.NativeObjects);
}
void OnSnapshotFinished(string path, bool success)
{
Debug.Log(success ? $"Snapshot saved: {path}" : "Snapshot failed");
}
}

  • ProfilerMarker를 코드 핵심 경로에 삽입하면 타임라인에서 정확한 병목 구간을 확인할 수 있다.
  • CPU 스파이크의 가장 흔한 원인은 GC.Alloc이다. 핫 경로에서 string 연결과 LINQ를 피한다.
  • GPU 병목은 Frame Debugger로 드로우콜 과다 여부를 먼저 확인하고, GPU Instancing과 Batching으로 대응한다.
  • Stats 패널의 SetPass Calls가 Batches보다 중요한 지표인 경우가 많다. 셰이더 variant 수를 최소화한다.