60FPS를 만들기 위한 조건
저녁에 친구가 60FPS를 만들기 위한 조건에 대한 얘기를 해줬다.
FPS는 Frame Per Second의 약자로 1초당 보여지는 프레임 수를 의미한다. 60FPS는 1초당 60프레임이 보여짐을 의미하며, 반대로 얘기하면 60프레임을 연산하는데 1초가 걸려야 한다는 이야기이다.
# 60F/S -> 1F/0.016S
즉, 1 프레임을 연산하는데는 1/60second 약 0.016second의 시간이 최대치이다. 1프레임당 이 이상의 연산 시간이 소요되면 60FPS를 보장할 수 없다.
참고로 0.016s는 16ms와 같다.
따라서 개발자는 16ms라는 제한된 시간을 각 연산에 적절히 분배해야 한다.
필요한 연산에는 여러가지 있는데, 만약 리스트에 1,000,000개의 int형 변수를 추가한다면 얼마나 걸릴까?
Visual Studio 2022 Test
using System.Diagnostics;
class Program
{
static void Main()
{
List<int> numbers = new List<int>();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 1000000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
}
}
7ms가 걸린다.
만약 합 연산을 백만번 실행한다면?
using System.Diagnostics;
class Program
{
static void Main()
{
int j;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 1000000; i++)
{
j = i + i;
}
stopwatch.Stop();
Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
}
}
3ms가 소요된다. 단순히 생각해서 프레임당 5,000,000번의 합연산까지는 60FPS를 보장하는데 문제가 없다는 얘기다. 심지어 이는 CPU만 사용했을 때 걸리는 시간이다. (내 컴퓨터 꽤 좋은듯?)
그럼 GPU는 어떨려나?
C# 기존 라이브러리로는 GPU 프로그래밍을 할려면 별도의 준비가 필요해서 유니티에서 진행해보겠다.
Unity Test
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
public class CPUCompute : MonoBehaviour
{
int j;
Stopwatch stopwatch = new Stopwatch();
private void Start()
{
stopwatch.Start();
for (int i = 0; i < 1000000; i++)
{
j = i + i;
}
stopwatch.Stop();
UnityEngine.Debug.Log($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
}
}
일단 앞서 진행했던 계산을 유니티에서 실행해본 결과...
1ms밖에 소요되지 않는다. 음? 왜 연산 속도에 차이가 발생하지?
개인적으로 추측하기론 유니티가 c#을 c++로 변환하는데서 속도 차이가 발생한다고 본다, 똑같은 합연산의 경우 C++의 속도가 월등하다. Visual studio로 같은 코드를 c++로 돌리면 0.4ms가 나온다.
그 다음 챗 gpt한테 부탁해서 GPU 연산 코드를 받아왔다.
// IntArrayComputeShader.compute
#pragma kernel CSMain
RWStructuredBuffer<int> resultBuffer;
[numthreads(256, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
resultBuffer[id.x] = id.x;
}
using UnityEngine;
using System.Diagnostics;
public class GPUCompute : MonoBehaviour
{
public ComputeShader computeShader;
private int[] data;
private ComputeBuffer computeBuffer;
void Start()
{
int dataSize = 1000000;
data = new int[dataSize];
// Compute Buffer 생성
computeBuffer = new ComputeBuffer(dataSize, sizeof(int));
computeShader.SetBuffer(0, "resultBuffer", computeBuffer);
// 연산 시간 측정을 위한 Stopwatch 생성
Stopwatch stopwatch = new Stopwatch();
// Compute Shader 실행 전 시간 측정 시작
stopwatch.Start();
// Compute Shader 실행
int threadGroups = Mathf.CeilToInt(dataSize / 256.0f);
computeShader.Dispatch(0, threadGroups, 1, 1);
// GPU 작업이 완료될 때까지 대기 (필요한 경우에만)
// AsyncGPUReadback.Request(computeBuffer, (request) => {});
// Compute Shader 실행 후 시간 측정 종료
stopwatch.Stop();
// 경과된 시간(틱)을 나노초로 변환
long elapsedTicks = stopwatch.ElapsedTicks;
double elapsedTimeNs = (elapsedTicks * (1e9)) / Stopwatch.Frequency;
// GPU에서 결과를 읽어옴
computeBuffer.GetData(data);
// Compute Buffer 해제
computeBuffer.Release();
// 결과 출력
UnityEngine.Debug.Log($"GPU 연산 시간: {elapsedTimeNs} ns");
UnityEngine.Debug.Log("첫 번째 값: " + data[0]); // 0
UnityEngine.Debug.Log("마지막 값: " + data[dataSize - 1]); // 999999
}
}
그 결과...
잘 봐야 한다. ms가 아니라 ns이다. ms로 측정하니까 0이 나온다.
그리고 60800ns는 0.06ms다!! 엄청나게 빠르다!
CPU 연산에 1,598,500 ns가 소요됬고 GPU 연산에 60,800 ns가 소요됬으며 GPU연산이 CPU연산보다 26배 빠르다.
추가
Unity에선 핸드폰의 경우 발열을 생각해서 30~40%의 여유를 남겨두라고 한다. 즉 핸드폰에서 60FPS를 만들려면 12ms정도의 연산 시간이 최대인 것이다.
결론
Visual Studio 2022 C# | 3ms |
Visual Studio 2022 C++ | 0.4ms |
Unity CPU | 1.6ms |
Unity GPU | 0.06ms |
60FPS를 보장하기 위해선 1Frame을 연산하는데 소요되는 시간이 최대 16ms여야 한다.
내 컴퓨터에서 1,000,000번의 합 연산을 CPU로 진행하는데 걸리는 시간은 C#이 3ms, C++이 0.4ms이다.
이는 단순하게 생각해서 내 컴퓨터에서 60FPS를 만드는데 C#은 약 5,000,000번의 합 연산까지, C++은 40,000,000번의 합 연산까지 허용된다는 얘기다.
Unity의 경우 1,000,000번의 합 연산을 CPU로 진행하는데 1.6ms가 소요되고 GPU로 진행하는대 0.06ms가 소요된다.