작성일 :

병목의 개념

Part 1에서 하나의 프레임이 CPU 단계와 GPU 단계로 이루어져 있고, 이 두 단계가 파이프라이닝을 통해 병렬로 실행됨을 살펴보았습니다. CPU가 프레임 N의 로직을 처리하는 동안 GPU는 프레임 N-1의 렌더링을 처리합니다.


이 병렬 구조에서 프레임 시간은 CPU 시간과 GPU 시간의 단순 합이 아닙니다. 둘 중 더 긴 쪽이 프레임 시간을 결정합니다.

\[\text{프레임 시간} = \max(\text{CPU 시간},\; \text{GPU 시간})\]

CPU가 8ms 걸리고 GPU가 12ms 걸리면 프레임 시간은 12ms입니다. CPU가 15ms 걸리고 GPU가 10ms 걸리면 프레임 시간은 15ms입니다. 느린 쪽이 전체 속도를 결정하며, 이 느린 쪽을 병목(Bottleneck)이라 부릅니다.


병목이 아닌 쪽을 최적화해도 프레임 시간은 줄어들지 않습니다. GPU가 12ms 걸리는 상황에서 CPU를 8ms에서 5ms로 줄여도 프레임 시간은 여전히 12ms입니다. 최적화는 병목이 어디인지 파악하는 데서 시작합니다.


병목 판별 CPU 시간 > GPU 시간 → CPU-bound GPU 시간 > CPU 시간 → GPU-bound CPU-bound 상태 CPU (15ms) GPU 대기 (10ms) 프레임 시간 = 15ms GPU-bound 상태 CPU 대기 (10ms) GPU (15ms) 프레임 시간 = 15ms


병목은 고정된 것이 아닙니다. 화면에 파티클과 이펙트가 많이 겹치면 GPU가 픽셀 계산에 시간을 많이 소모하여 GPU-bound가 되고, AI 에이전트가 대량으로 경로를 탐색하면 CPU가 로직 계산에 시간을 많이 소모하여 CPU-bound가 됩니다. 같은 게임에서도 장면과 상황에 따라 병목이 바뀝니다.


CPU-bound

CPU가 프레임 시간을 결정하는 상태, 즉 CPU-bound에서는 GPU가 렌더링을 먼저 마치고도 다음 프레임의 렌더링 명령을 받지 못해 유휴 상태에 머무릅니다.

CPU가 처리하는 작업

하나의 프레임에서 CPU는 물리, 스크립트 로직, 애니메이션, 렌더링 준비 등 여러 서브시스템을 순서대로 처리합니다. Unity 프로파일러로 프레임을 열어보면 메인 스레드의 타임라인에서 이 흐름을 직접 확인할 수 있습니다.


CPU 메인 스레드의 한 프레임 FixedUpdate + Physics (0~N회 반복) Update 애니메이션 (Animator 평가) LateUpdate UI (Canvas Rebuild) 렌더 준비 Culling, Batching, 커맨드 버퍼 CPU 프레임 시간


FixedUpdate + Physics가 프레임에서 가장 먼저 실행됩니다. FixedUpdate는 고정된 시간 간격(기본 0.02초)마다 호출되므로, 프레임 시간에 따라 한 프레임에서 0회 또는 여러 회 실행될 수 있습니다. 각 FixedUpdate 호출 직후에 PhysX 물리 엔진이 충돌 감지와 강체(Rigidbody) 시뮬레이션을 수행합니다.


물리 처리가 끝나면 Update가 실행됩니다. AI 의사결정, 입력 처리, 게임 규칙 평가 등 MonoBehaviour의 게임플레이 코드가 이 단계에서 동작합니다.


애니메이션 업데이트는 두 단계로 나뉩니다.

  • 상태 평가 + 뼈대 계산 (CPU) — Animator 컴포넌트가 각 캐릭터의 애니메이션 상태를 평가하고, 뼈대(Bone)의 위치와 회전을 계산합니다.
  • 스키닝 (GPU) — 결정된 뼈대 위치에 맞춰 캐릭터 메쉬의 정점을 변형합니다. 설정에 따라 CPU에서 처리할 수도 있지만, 일반적으로 GPU에 위임합니다.


LateUpdate는 Update와 애니메이션 이후에 실행됩니다. 카메라가 캐릭터를 따라가는 로직처럼, 다른 오브젝트의 최종 위치가 확정된 뒤에 실행해야 하는 코드가 여기에 해당합니다.

LateUpdate 이후, 렌더링이 시작되기 직전에 UI 레이아웃 계산이 수행됩니다. Canvas 시스템이 UI 요소의 위치, 크기, 배치를 결정하는 단계입니다. Canvas는 자식 UI 요소들의 메쉬를 하나로 합쳐서 배칭하는데, UI 요소 하나만 바뀌어도 이 합쳐진 메쉬 전체를 다시 생성하는 리빌드(Rebuild)가 발생합니다. 체력바나 대미지 숫자처럼 매 프레임 변하는 UI 요소가 많으면, Canvas 리빌드 비용만으로도 프레임 예산의 상당 부분을 차지할 수 있습니다.


렌더 준비 단계에서 CPU는 GPU에 보낼 렌더링 명령을 만듭니다. 이 과정은 크게 세 단계로 나뉩니다.

먼저 컬링(Culling)에서 GPU에 보낼 필요가 없는 오브젝트를 걸러냅니다. 프러스텀 컬링(Frustum Culling)은 카메라의 절두체(보이는 영역) 밖에 있는 오브젝트를 제외하고, 오클루전 컬링(Occlusion Culling)은 절두체 안에 있지만 다른 오브젝트에 완전히 가려진 오브젝트를 추가로 제외합니다. 오브젝트 수가 많을수록 이 판별 자체에 CPU 시간이 더 소요됩니다.

다음으로 소팅과 배칭(Sorting & Batching)이 수행됩니다. 컬링을 통과한 오브젝트들을 렌더링 순서에 맞게 정렬하고, 같은 머티리얼을 사용하는 오브젝트를 묶어 드로우 콜 수를 줄입니다. 드로우 콜은 GPU에 “이 오브젝트를 그려라”라는 명령을 보내는 것으로, 하나하나에 CPU 오버헤드가 따르기 때문에 횟수를 줄이는 것이 중요합니다.

마지막으로 이 렌더링 명령들을 커맨드 버퍼(Command Buffer)에 순서대로 기록하여 GPU에 전달합니다. “이 셰이더를 사용해라”, “이 메쉬를 그려라”, “렌더 타겟을 전환해라” 같은 명령이 여기에 담깁니다. 렌더링할 오브젝트가 많을수록 컬링, 소팅, 배칭의 CPU 비용이 높아지고, 커맨드 버퍼에 기록할 명령도 늘어납니다.

정리하면, 렌더 준비는 보이지 않는 오브젝트를 걸러내고(컬링), 남은 오브젝트를 정렬·묶어 드로우 콜을 줄이고(소팅·배칭), 최종 명령을 커맨드 버퍼에 기록하여 GPU에 넘기는 과정입니다.

CPU-bound의 특징

CPU-bound 상태에서는 화면의 렌더링 복잡도를 낮춰도 프레임 시간이 개선되지 않습니다. 해상도를 절반으로 줄이거나, 셰이더를 단순화하거나, 후처리를 꺼도 GPU 시간만 줄어들 뿐이고 프레임 시간은 CPU 시간에 의해 여전히 제한됩니다.


반대로, 스크립트 로직을 최적화하거나 물리 오브젝트 수를 줄이면 프레임 시간이 직접적으로 개선됩니다.

CPU-bound에서 GPU 최적화의 효과 최적화 전 CPU (15ms) GPU (10ms) 프레임 = 15ms GPU 해상도 절반 CPU (15ms) ← 변화 없음 GPU (6ms) 프레임 = 15ms ← 변화 없음 CPU 스크립트 최적화 CPU (9ms) ← 개선 GPU (10ms) 프레임 = 10ms ← 개선 (GPU-bound로 전환)


위 예시에서 CPU를 최적화하자 병목이 CPU에서 GPU로 옮겨간 것을 확인할 수 있습니다. 한쪽 병목을 해소하면 다른 쪽이 새로운 병목이 됩니다. 양쪽을 번갈아 최적화하는 과정이 반복됩니다.


GPU-bound

GPU가 프레임 시간을 결정하는 상태, 즉 GPU-bound에서는 CPU가 다음 프레임의 로직을 이미 끝냈지만 GPU가 아직 이전 프레임의 렌더링을 마치지 못해 CPU가 유휴 상태에 머무릅니다.

GPU가 처리하는 작업

GPU는 CPU가 만든 렌더링 명령을 받아 화면에 그릴 픽셀을 만들어냅니다. 이 과정은 정점 처리, 래스터라이징, 프래그먼트 처리, 출력 병합의 네 단계를 거칩니다.

GPU 렌더링 파이프라인 (간략화) CPU가 보낸 명령 정점 처리 (Vertex) 정점 셰이더 실행 좌표 변환, 조명 준비 래스터라이징 삼각형을 픽셀로 변환 프래그먼트 처리 (Pixel) 각 픽셀의 색상 계산 텍스처 샘플링, 라이팅 출력 병합 (Output) 깊이 테스트, 블렌딩 프레임 버퍼에 기록


GPU 시간을 좌우하는 주요 요인은 다음과 같습니다.

먼저 렌더 스테이트(Render State) 변경입니다. GPU는 드로우 콜을 처리할 때 현재 적용 중인 셰이더, 텍스처, 블렌딩 모드 등의 설정을 참조하는데, 이 설정 묶음이 렌더 스테이트입니다. 연속된 드로우 콜이 같은 설정을 공유하면 변경 없이 처리되지만, 셰이더, 머티리얼, 렌더 타겟 등이 바뀔 때마다 GPU가 파이프라인을 재설정하는 비용이 발생합니다. 특히 셰이더 변경이 가장 비용이 큽니다.


다음으로 셰이더 연산입니다. 위 파이프라인에서 정점 처리와 프래그먼트 처리 단계가 셰이더가 실행되는 구간입니다. 정점 셰이더는 메쉬의 각 정점 위치를 변환하고, 프래그먼트 셰이더는 화면의 각 픽셀 색상을 계산합니다. 셰이더가 복잡할수록(라이팅 모델, 노멀 맵, 반사 계산 등) GPU 연산량이 늘어납니다.


다음으로 텍스처 샘플링입니다. 셰이더가 텍스처에서 색상 값을 읽는 동작으로, 특히 프래그먼트 셰이더에서 가장 빈번하게 발생하며 GPU의 메모리 대역폭을 소모합니다. 밉맵(Mipmap)은 원본 텍스처를 절반씩 축소한 사본들을 미리 만들어 두는 기법으로, 멀리 있는 오브젝트에는 작은 밉맵을 읽어 대역폭을 절약합니다. 밉맵 없이 항상 원본 해상도를 읽거나, 고해상도 텍스처를 여러 장 동시에 읽으면 대역폭 소모가 급격히 늘어납니다.


마지막으로 후처리(Post-Processing)가 있습니다. 위 파이프라인을 거쳐 완성된 이미지에 추가 효과를 적용하는 단계입니다. 밝은 부분을 번지게 하는 블룸(Bloom), 계단 현상을 줄이는 안티앨리어싱(Anti-Aliasing), 색감을 조정하는 색보정(Color Grading), 틈새에 그림자를 넣는 앰비언트 오클루전(Ambient Occlusion) 등이 여기에 해당합니다. 각 효과는 전체 화면 픽셀을 한 번 이상 읽고 쓰므로 비용이 누적됩니다.

모바일에서 GPU-bound가 흔한 이유

모바일 기기의 GPU는 데스크톱 GPU와 설계 철학이 다릅니다. 데스크톱 GPU는 전력 제한이 크지 않으므로 순수 연산 성능을 극대화하는 방향으로 발전해 왔습니다. 반면 모바일 GPU는 배터리로 동작하므로 와트당 성능(Performance per Watt)을 최우선으로 설계합니다. 이 설계 차이가 두 가지 제약으로 이어집니다.


첫째, 필레이트(Fill Rate) 제한입니다. 필레이트는 GPU가 초당 채울 수 있는 픽셀 수입니다. 모바일 디스플레이의 해상도는 이미 2560x1440(QHD) 이상까지 올라갔지만, 모바일 GPU의 필레이트는 데스크톱 GPU보다 크게 낮습니다. 세대와 등급에 따라 차이가 다르지만, 일반적으로 데스크톱 대비 수분의 1 수준입니다. 화면 해상도에 비해 GPU의 픽셀 처리 능력이 부족한 상황이 쉽게 발생합니다.

이 필레이트 제한에 오버드로우(Overdraw)가 겹치면 문제가 심각해집니다. 오버드로우란 같은 픽셀 위치를 여러 번 그리는 것입니다. 불투명 오브젝트는 깊이 테스트로 가려진 픽셀을 건너뛸 수 있지만, 반투명 오브젝트는 뒤에 있는 색상과 섞어야 하므로 뒤에서 앞으로(Back-to-Front) 순서로 모두 그려야 합니다. 같은 픽셀이 반투명 오브젝트 수만큼 반복 처리됩니다. 필레이트가 제한된 모바일에서 오버드로우는 프레임 시간을 직접적으로 늘립니다.


둘째, 메모리 대역폭 제한입니다. 데스크톱 GPU는 자신만 사용하는 전용 메모리인 VRAM을 가지고 있고, GPU와 VRAM 사이를 수백 GB/s 대역폭의 고속 버스로 연결합니다. 텍스처, 메쉬 데이터, 프레임 버퍼 등 렌더링에 필요한 데이터를 GPU가 빠르게 읽고 쓸 수 있습니다.

모바일 기기는 구조가 다릅니다. CPU, GPU, 그리고 메모리가 하나의 칩(SoC) 안에 통합되어 있고, CPU와 GPU가 같은 물리 메모리(LPDDR)를 공유합니다. 이 구조를 통합 메모리(Unified Memory)라고 합니다. CPU와 GPU가 같은 메모리 버스를 나눠 쓰기 때문에 실제로 GPU가 사용할 수 있는 대역폭은 더 줄어듭니다. 이 공유 버스의 대역폭은 수십 GB/s 수준으로 데스크톱 GPU의 전용 버스보다 크게 낮아, GPU 코어의 연산 능력은 남아 있는데 데이터를 읽어오지 못해 대기하는 상황이 발생합니다.

데스크톱 GPU GPU 고속 전용 버스 (수백 GB/s) VRAM (전용) 모바일 SoC CPU GPU 공유 버스 (수십 GB/s) 공유 메모리 (LPDDR)


필레이트와 대역폭 모두 제한된 모바일에서는 셰이더 복잡도, 텍스처 해상도, 후처리 수, 오버드로우를 데스크톱보다 엄격하게 관리해야 합니다.


VSync

프레임 시간이 16.67ms 안에 끝난다고 해서 바로 60fps로 표시되는 것은 아닙니다. GPU가 프레임을 완성한 시점과 디스플레이가 그 프레임을 표시하는 시점 사이에 VSync(Vertical Synchronization)라는 제약이 있기 때문입니다.

디스플레이의 동작 원리

디스플레이는 화면을 주기적으로 갱신합니다. 60Hz 디스플레이라면 초당 60번, 즉 16.67ms마다 한 번씩 화면을 새로 그립니다. 갱신 방식은 왼쪽 위부터 오른쪽 아래까지 한 줄씩 스캔하는 것이며, 한 화면을 다 그리면 다시 왼쪽 위로 돌아가 다음 갱신을 시작합니다.

이 갱신 주기는 GPU가 프레임을 얼마나 빨리 만드는지와 무관하게, 디스플레이의 하드웨어 클럭에 의해 고정되어 있습니다.

티어링(Tearing)

일반적으로 GPU는 두 개의 프레임 버퍼(Frame Buffer)를 사용합니다. 디스플레이가 현재 읽고 있는 프론트 버퍼(Front Buffer)와, GPU가 다음 프레임을 렌더링하는 백 버퍼(Back Buffer)입니다. 프레임이 완성되면 두 버퍼를 교체(swap)하는데, VSync 없이 이 교체가 아무 때나 일어나면 문제가 발생합니다.

디스플레이가 프론트 버퍼에서 프레임 N의 위쪽 절반을 스캔하고 있는 도중에 버퍼가 교체되면, 나머지 아래쪽 절반은 프레임 N+1의 데이터로 스캔됩니다. 결과적으로 한 화면에 두 프레임의 이미지가 나뉘어 보입니다.

티어링(Tearing) 프레임 N 프레임 N+1 ← 디스플레이 스캔이 여기까지 진행된 시점에 ← GPU가 프레임 버퍼를 교체 ← 아래쪽은 새 프레임으로 표시 화면이 수평으로 찢어져 보임


이 현상을 티어링(Tearing)이라 부릅니다. 티어링은 항상 발생할 수 있지만, 카메라가 정지해 있거나 느리게 움직이면 연속된 두 프레임의 이미지가 거의 동일하므로 경계선이 눈에 띄지 않습니다. 반면 카메라가 빠르게 회전하거나 오브젝트가 빠르게 이동하면 프레임 N과 N+1의 이미지 차이가 커지고, 경계선을 기준으로 위아래가 수평으로 어긋나 보입니다.

VSync의 동작

VSync는 GPU의 프레임 제출을 디스플레이의 갱신 주기에 동기화합니다. 디스플레이가 한 화면을 다 스캔하고 다음 스캔을 시작하기 전에는 짧은 빈 시간이 있습니다. 이 구간을 수직 귀선 구간(VBI, Vertical Blanking Interval)이라 부르며, V-BLANK라고도 합니다. VSync가 켜져 있으면 GPU가 프레임을 완성하더라도 VBI까지 기다렸다가 프론트 버퍼와 백 버퍼를 교체합니다.

VSync 동작 (60Hz) 0ms 16.67ms 33.33ms 50.00ms 화면 스캔 VB 스캔 VB 스캔 VB GPU A (12ms) 대기 버퍼 교체 B (14ms) 버퍼 교체 C (12ms) 대기 버퍼 교체 = VBlank 대기 (프레임 완성 후 교체를 기다리는 시간) 프레임이 일찍 완성되어도 VBlank까지 대기 후 표시 디스플레이 스캔 도중 버퍼가 바뀌지 않으므로 티어링 없음


VSync를 켜면 버퍼 교체가 VBlank에서만 일어나므로 티어링이 사라집니다. 다만 이 동기화에는 대가가 따릅니다.

VSync의 프레임 드롭 현상

60Hz 디스플레이에서 VSync가 켜져 있으면, 버퍼 교체는 16.67ms 간격의 VBlank 시점에만 일어납니다. 프레임 처리가 매번 16.67ms 안에 끝나면 매 VBlank마다 새 프레임이 표시되어 60fps가 유지됩니다.


프레임 하나가 16.67ms를 초과하면 상황이 달라집니다. 이전 프레임이 VBlank에서 교체된 직후 렌더링을 시작하는데, 완성까지 18ms가 걸리면 다음 VBlank(16.67ms 후)에는 아직 렌더링 중입니다. 그 VBlank을 놓치고 그 다음 VBlank까지 기다려야 하므로, 이전 프레임으로부터 33.33ms 후에야 표시됩니다. 1.3ms만 초과했을 뿐인데 프레임률이 60fps에서 30fps로 절반이 되는 것입니다.

VSync에서의 프레임 드롭 0ms 16.67ms 33.33ms 50.00ms 66.67ms 화면 스캔 VB 스캔 VB 스캔 VB 스캔 VB GPU A (12ms) A 표시 B (18ms) X 대기 B 표시 C (10ms) 대기 C 표시 A: 12ms 완성 → 16.67ms VBlank에 표시 (60fps) B: 18ms 완성 → 33.33ms VBlank 놓침(X) → 50.00ms에 표시 (30fps로 드롭) C: 10ms 완성 → 66.67ms VBlank에 표시 (60fps 복귀)


60fps와 30fps 사이에 중간 단계가 없으므로, 대부분의 프레임이 14ms인데 가끔 17ms짜리가 섞이면 해당 프레임이 30fps로 표시되면서 플레이어 눈에는 부드러운 60fps 중간에 갑작스러운 끊김으로 보입니다.

VSync의 이 계단식 드롭 특성 때문에, 60fps를 유지하려면 평균 프레임 시간이 아니라 개별 프레임 하나하나가 16.67ms 안에 끝나야 합니다.


프레임 페이싱

VSync는 티어링을 방지하고, 프레임 드롭도 예산 관리로 막을 수 있습니다. 하지만 모든 프레임이 16.67ms 안에 끝나더라도 플레이어가 체감하는 부드러움에는 또 다른 요소가 있습니다. 균일한 프레임 간격입니다.

불균일한 프레임 간격의 문제

프레임마다 처리 시간이 조금씩 다릅니다. 어떤 프레임은 10ms, 다음은 15ms, 그 다음은 11ms가 걸립니다. 모두 16.67ms 안에 들어오면 VBlank마다 한 프레임씩 교체되어 화면 갱신 간격은 균일합니다.

하지만 대부분의 프레임이 14~15ms에 완성되는 게임이라면 여유가 1~2ms밖에 없습니다. 특정 프레임에서 파티클이 한꺼번에 생성되거나 오브젝트가 대량으로 활성화되면, 처리 시간이 16.67ms를 살짝 넘길 수 있습니다. 이 프레임은 직전 VBlank을 놓치고 다음 VBlank(33.33ms 후)까지 기다려야 합니다.

불균일한 프레임 페이싱 0 16.67 33.33 50.00 66.67 83.33 이상적 A B C D E 16.67 16.67 16.67 16.67 16.67 ms 불균일 A B C D E 16.67 33.33 (!) 16.67 16.67 ms ms 단위 간격 — 불균일 행에서 B가 VBlank을 놓쳐 다음 VBlank까지 대기


B의 처리 시간이 17ms로 16.67ms를 살짝 넘기면, B는 직전 VBlank을 놓치고 다음 VBlank에서야 표시됩니다. 그 사이 A가 화면에 33.33ms 동안 머무릅니다. B가 표시된 직후 C는 16.67ms 만에 바로 다음 VBlank에 표시됩니다. 결과적으로 A→B 간격은 33.33ms, B→C 간격은 16.67ms가 되어 프레임 간격이 들쭉날쭉해집니다. 프레임레이트 카운터는 평균 60fps를 보여주지만, 화면은 버벅거려 보입니다.

사람의 시각은 평균 프레임레이트보다 프레임 간격의 일관성에 더 민감합니다. 40fps가 일정하게 유지되는 것이, 60fps인데 간간이 프레임이 늦게 표시되는 것보다 부드럽게 느껴집니다.

프레임 페이싱의 역할

이 문제를 해결하기 위해 등장한 것이 프레임 페이싱(Frame Pacing)입니다. 프레임이 균일한 간격으로 표시되도록 GPU의 작업 제출 타이밍을 조절하는 기술입니다.


프레임 페이싱 없이 엔진이 프레임을 최대한 빨리 처리하면, 프레임마다 완성 시점이 제각각이 되어 앞에서 본 것처럼 간격이 들쭉날쭉해집니다. 프레임 페이싱은 디스플레이의 VSync 신호를 기준으로 “지금 다음 프레임을 시작하라”는 타이밍을 엔진에 알려줍니다. 엔진이 이 신호에 맞춰 프레임 작업을 시작하면, 각 프레임의 시작 시점이 디스플레이 주기에 정렬되어 표시 간격이 균일해집니다.

모바일 플랫폼에서는 이 신호를 OS가 제공합니다. Android의 Choreographer, iOS의 CADisplayLink가 디스플레이 갱신 주기에 맞춰 콜백을 발생시키며, Unity 엔진은 이 콜백을 받아 프레임 작업을 시작합니다.

프레임 페이싱 없음 0ms 16.67ms 33.33ms 50.00ms VSync GPU A (12ms) B (8ms) C (18ms) D (6ms) A 표시 B 표시 C 표시 프레임 완성 후 바로 다음 프레임 시작 → 시작 타이밍이 VSync와 무관 → 표시 간격 불균일 프레임 페이싱 적용 0ms 16.67ms 33.33ms 50.00ms VSync GPU A (12ms) B (8ms) C (14ms) A 표시 B 표시 C 표시 매 VSync에 맞춰 프레임 시작 페이싱 없음: 16.67 → 16.67 → 33.33ms (불균일) 페이싱 적용: 16.67 → 16.67 → 16.67ms (균일) ░ = VBlank 대기 — 프레임 페이싱은 GPU 작업 제출을 VSync에 정렬


Unity에서는 Android의 경우 Optimized Frame Pacing 옵션으로 이 기능을 활성화할 수 있습니다. 이 옵션이 켜지면 Unity가 Android Frame Pacing 라이브러리를 사용하여 프레임 제출 타이밍을 디스플레이 주기에 맞춥니다. iOS의 경우 Unity가 CADisplayLink를 통해 프레임 페이싱을 자동으로 처리하므로 별도 설정이 필요하지 않습니다.

목표 프레임레이트와 프레임 페이싱의 관계

60fps가 안정적으로 나오지 않는 모바일 기기에서 무리하게 60fps를 목표로 잡으면, VSync의 계단식 드롭 때문에 60fps와 30fps를 오가는 불안정한 상태가 됩니다. 처음부터 30fps로 목표를 낮추고 프레임 페이싱을 적용하면 균일한 33.33ms 간격으로 프레임이 표시되어, 프레임레이트 숫자는 낮아도 체감은 오히려 부드러워집니다.

불안정한 60fps (목표 16.67ms, 일부 프레임 초과) 0ms 16.67 33.33 50.00 66.67 83.33 100.00 VSync X X GPU A B (18ms) C D (19ms) A 표시 B 표시 C 표시 D 표시 표시 간격: 16.67 → 33.33 → 16.67 → 33.33ms (들쭉날쭉) 안정적인 30fps (목표 33.33ms) 0ms 33.33ms 66.67ms 100.00ms VSync GPU A (25ms) B (28ms) C (22ms) A 표시 B 표시 C 표시 표시 간격: 33.33 → 33.33 → 33.33ms (균일, 부드러운 체감) 불안정한 60fps는 들쭉날쭉한 간격으로 버벅임 — 안정적 30fps는 균일한 간격으로 부드러움 ░ = VBlank 대기 구간


이처럼 VSync와 프레임 페이싱의 특성을 고려하면, 모바일 게임에서 프레임레이트 목표를 결정할 때는 최대 fps가 아니라 안정적으로 유지할 수 있는 fps를 기준으로 삼아야 합니다.


서멀 쓰로틀링

지금까지는 CPU와 GPU의 처리 능력이 게임 실행 내내 일정하다는 전제로 이야기했습니다. 데스크톱에서는 대체로 맞지만, 모바일 기기에서는 그렇지 않습니다.

피크 성능과 지속 성능

모바일 SoC(System on Chip)는 CPU, GPU, 메모리 컨트롤러가 손가락 끝만 한 칩 위에 밀집되어 있습니다. 전력을 많이 쓰면 열이 나고, 열을 방출할 수 있는 면적과 방열 구조가 데스크톱에 비해 제한적입니다. 팬이 없고, 방열판도 작거나 없습니다.

기기를 처음 켜거나 가벼운 작업 후에는 칩 온도가 낮아 최대 클럭으로 동작합니다. 이때의 성능을 피크 성능(Peak Performance)이라 부르며, 벤치마크 앱이 측정하는 수치가 바로 이 피크 성능입니다.

하지만 게임처럼 CPU와 GPU를 동시에 고부하로 사용하면 수 분 내에 칩 온도가 올라갑니다. 온도가 임계치에 도달하면 SoC는 스스로 클럭을 낮춥니다. 이 현상이 서멀 쓰로틀링(Thermal Throttling)입니다.

서멀 쓰로틀링에 의한 성능 저하 성능 100% 80% 60% 40% 시간 (분) 0 2 4 6 8 10 12 14 ~57% 피크 성능 (100%) 지속 성능 (50~70%)


피크 성능 대비 지속 성능(Sustained Performance)의 비율은 기기마다 다르지만, 일반적으로 50~70% 수준입니다. 피크 성능 기준으로 60fps가 나오던 게임이 10분 플레이 후 30~40fps로 떨어질 수 있습니다.

쓰로틀링이 프레임에 미치는 영향

서멀 쓰로틀링으로 클럭이 60%로 떨어지면, 이전까지 10ms 걸리던 CPU 작업은 약 17ms, 12ms 걸리던 GPU 작업은 약 20ms로 늘어납니다. 프레임 시간이 20ms가 되어 16.67ms를 초과하므로, 앞에서 살펴본 VSync 드롭이 발생합니다.

쓰로틀링 전후 비교 쓰로틀링 전 (피크 성능) 0ms 12ms 16.67ms CPU 10ms GPU 12ms 16.67ms 이내 60fps 쓰로틀링 후 (클럭 60%) 0ms 16.67ms 33.33ms CPU 17ms GPU 20ms X 16.67ms 초과 30fps 드롭


플레이어 입장에서는 게임 시작 직후에는 부드럽다가, 몇 분이 지나면서 점차 버벅거리기 시작합니다.

피크 성능이 아닌 지속 성능을 기준으로 설계

모바일 게임의 성능 목표는 피크 성능이 아니라 지속 성능을 기준으로 잡아야 합니다. 피크 성능 기준으로 60fps를 목표로 잡으면, 쓰로틀링이 시작되는 순간 프레임률이 급격히 떨어집니다. 지속 성능 기준으로 목표를 설정하면 플레이 시간에 관계없이 일정한 프레임률을 유지할 수 있습니다.

여기에 더해, 런타임에서 기기 온도를 감지하여 부하를 동적으로 조절하는 방법도 있습니다. 온도가 높아지면 그림자 해상도를 줄이거나, 후처리 효과를 끄거나, 렌더링 해상도를 낮추는 식으로 GPU 부하를 단계적으로 낮춰 쓰로틀링 자체를 지연시킵니다.


프레임 병목 진단의 흐름

병목 판별, VSync, 프레임 페이싱, 서멀 쓰로틀링을 하나의 진단 흐름으로 연결하면 다음 구조가 됩니다.

프레임 성능 진단 흐름 1. 목표 프레임레이트 설정 지속 성능 기준 (피크 성능 X) 2. 프레임 시간 측정 목표 달성? 균일한 간격? 완료 아니오 프레임 페이싱 설정 확인 아니오 3. 병목 판별 CPU 시간 > GPU → CPU-bound GPU 시간 > CPU → GPU-bound 4. 병목 서브시스템 식별 CPU-bound 스크립트 / 물리 / 애니메이션 / UI GPU-bound 셰이더 / 필레이트 / 대역폭 / 후처리 5. 서브시스템 최적화 다시 2번으로


쓰로틀링을 고려하면 2번(프레임 시간 측정)의 조건이 중요해집니다. 기기를 막 켜서 칩 온도가 낮은 상태, 즉 콜드 스타트(Cold Start) 직후에 측정하면 피크 성능이 측정되어 실제 플레이 환경과 다른 결과가 나옵니다. 기기를 충분히 예열한 상태(10분 이상 게임 실행 후)에서 측정해야 지속 성능 기준의 결과를 얻을 수 있습니다.


마무리

CPU-bound와 GPU-bound의 구분에서 출발하여, 각 병목에서 어떤 서브시스템이 프레임 시간을 잡아먹는지 살펴보았습니다. 여기에 VSync의 계단식 드롭, 프레임 페이싱, 모바일 고유의 필레이트·대역폭 제한, 서멀 쓰로틀링까지 더하면 프레임 성능 진단의 전체 그림이 완성됩니다.

  • 프레임 시간은 CPU 시간과 GPU 시간 중 더 긴 쪽이 결정합니다. 병목이 아닌 쪽을 최적화해도 프레임 시간은 줄어들지 않습니다.
  • CPU-bound일 때는 스크립트, 물리, 애니메이션, UI, 렌더 준비 중 원인을 식별합니다. GPU-bound일 때는 렌더 스테이트 변경, 셰이더 복잡도, 텍스처 샘플링, 필레이트, 후처리를 점검합니다.
  • 모바일 GPU는 필레이트 제한과 공유 메모리 대역폭 제한이라는 구조적 제약이 있어 GPU-bound가 발생하기 쉽습니다.
  • VSync는 티어링을 방지하지만, 프레임 예산을 초과하면 프레임레이트가 계단식으로 떨어집니다(60→30→20fps).
  • 프레임 페이싱은 프레임 시작 시점을 VSync에 정렬하여 표시 간격을 균일하게 유지합니다. 불안정한 60fps보다 안정적인 30fps가 체감 품질이 높습니다.
  • 서멀 쓰로틀링은 지속 성능을 피크 대비 50~70%로 떨어뜨립니다. 모바일 게임의 성능 목표는 지속 성능 기준으로 설정해야 합니다.

한쪽 병목을 해소하면 다른 쪽이 새로운 병목이 됩니다. 모바일에서는 시간이 지나면서 쓰로틀링까지 겹칩니다. 측정과 최적화를 반복하는 것이 프레임 성능을 안정적으로 유지하는 유일한 방법입니다.


렌더링 기초 (1)에서는 GPU가 처리하는 데이터의 첫 번째 요소인 메쉬의 구조를 살펴봅니다.



관련 글

시리즈

전체 시리즈

Tags: Unity, 게임루프, 모바일, 최적화, 프레임레이트

Categories: ,