2. Burst Compiler
- Burst 컴파일러는 유니티의 성능을 끌어올리기 위한 컴파일러 도구이다. C# 코드와 호환되는 최적화된 네이티브 CPU 코드를 작성할 때 필요하다.
- Burst는 Unity에서 고성능 연산 처리를 위해 도입된 컴파일러로, Unity에서 사용 가능한 C# 코드 컴파일러이다. Burst는 LLVM을 통해 .NET IL( Intermediate Language, 중간 언어)를 대상 CPU 아키텍처에 최적화된 코드로 변환한다.
- Unity의 Job System을 위해 디자인되었다.
- 사용 방법은 단순히 클래스에 어트리뷰트로 [BurstCompiler]를 붙이면 된다. 다만 주의사항이 있다면, Burst Compiler를 사용하면 런타임 오류가 발생했을 때 상세 정보를 확인하기 어렵다는 문제가 있다.
[BurstCompiler]
public struct SquaresJob : IJob
{
public NativeArray<int> Nums;
public void Execute()
{
for (int i = 0; i < Nums.Length; i++)
{
Nums[i] *= Num[i];
}
}
}
- 타입: int, float 같은 기본 타입은 사용할 수 있지만 string은 사용할 수 없다. 또, 조금 복잡한 타입은 Unity.Mathematics에서 지원하는 타입을 사용해야 한다.
- ex) Vector3 대신 float3를 사용하는 식
- System.Collections에서 지원하는 리스트 List나 Dictionary는 사용할 수 없다. 대신, Unity.Collections에 있는 Native Container를 사용해야 한다.
- ex) NativeArray나 NativeHashMap 같은 것을 통해 데이터를 관리한다(병렬 처리 여부에 따라 또 변수가 달라짐).
// 출처: https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/getting-started.html
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
public class MyBurst2Behavior : MonoBehaviour
{
void Start()
{
var input = new NativeArray<float>(10, Allocator.Persistent);
var output = new NativeArray<float>(1, Allocator.Persistent);
for (int i = 0; i < input.Length; i++)
input[i] = 1.0f * i;
var job = new MyJob
{
Input = input,
Output = output
};
job.Schedule().Complete();
Debug.Log("The result of the sum is: " + output[0]);
input.Dispose();
output.Dispose();
}
// Using BurstCompile to compile a Job with Burst
[BurstCompile]
private struct MyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public void Execute()
{
float result = 0.0f;
for (int i = 0; i < Input.Length; i++)
{
result += Input[i];
}
Output[0] = result;
}
}
}
MyBurst2Behavior 클래스는 MyJob이라는 Job을 호출하는 클래스이다.
MyJob은 구조체이기 때문에 여기에서 처리할 입력, 출력 데이터를 전달해 주어야 한다. 그 역할을 NativeArray<float> Input, Output이 수행하게 된다. 이때 두 개의 네임스페이스를 포함하는 것을 볼 수 있다.
- using Unity.Burst: Burst Compiler를 사용하기 위해 필요한 네임스페이스
- using Unity.Collections: NativeArray와 같은 Native Container를 사용하려면 필요한 네임스페이스
이때 NativeArray는 메모리를 C#에서 자동으로 관리해 주지 않기 때문에 직접 할당과 해제를 프로그래머가 제어해야 한다. 먼저 Native Container를 할당하는 부분은 이렇게 생겼다.
var input = new NativeArray<float>(10, Allocator.Persistent);
var output = new NativeArray<float>(1, Allocator.Persistent);
input은 길이 10의 배열이고, output은 길이 1의 배열이다.
이때 Native Container의 두 번째 인자는 Allocator의 타입을 의미한다. 이 부분은 조금 있다가 더 자세히 살펴보자.
다음으로, Job에 해당 데이터를 전달하고 나서, 끝나고 나서는 Native Container의 해제가 필요하다.
input.Dispose();
output.Dispose();
Native Container는 위와 같이 Dispose()를 호출하면 해제할 수 있다. C++에서의 free와 동일하다고 볼 수 있겠다.
그런데 이때 위를 보면 한 프레임만에 할당하고 해제하는 것을 볼 수 있는데, 꼭 그렇지 않고 오브젝트가 파괴될 때까지 Native Container를 유지하고 싶을 수도 있다.
그러고 싶을 땐 OnDestroy()나 프로그램이 종료될 때 Dispose해도 되긴 하는데 이때 고려해야 하는 것이 바로 Native Container의 Allocator이다. 이 Allocator에 따라 Native Container의 수명이 달라지기 때문이다. Unity Docs의 내용을 참고하자면 다음과 같다.
- Allocator.Temp: 한 프레임 이하의 수명을 가진 할당. 할당 속도가 가장 빠르지만, Temp를 쓰면 NativeContainer 할당을 잡에 전달할 수 없다(Job이 완료되기도 전에 자동으로 해제되기 때문). 자동으로 해제되지만, 가능하면 직접 Dispose를 호출하는 것을 권장한다.
- Allocator.TempJob: 4프레임 내의 스레드 세이프 할당에 사용한다. Temp 보다 할당 속도가 더 느리지만 Persistent 보다는 속도가 더 빠르다. 중요한 점은, 이러한 타입의 할당은 반드시 4프레임 내에서 Dispose 메서드를 호출해야 한다(그러지 않을 경우 경고가 발생하니 참고할 것). 대부분의 소규모 잡은 이러한 NativeContainer 할당 타입을 사용한다.
- Allocator.Persistent: 애플리케이션의 주기에 걸쳐 필요한 만큼 오래 지속된다. 대신 할당이 가장 느리다. C언어의 malloc을 직접 호출하는 래퍼 역할을 한다. 오래 걸리는 잡은 이 NativeContainer 할당 타입을 사용할 수 있긴 하지만, 성능이 중요한 상황에서는 Persistent를 사용하면 안 된다니 주의할 것.
즉, 보통 Job에 필요한 Native Container는 Allocator.TempJob(4프레임)이나 Allocator.Persistent(영구적)를 사용할 수 있는데, 성능적으로는 TempJob이 더 유리하다.
참고 자료
Burst compiler - Unity Manual
NativeContainer - Unity Docs
'Unity3D > API & Library' 카테고리의 다른 글
Unity DOTS 기술 정리: 1. Job System (0) | 2025.09.06 |
---|---|
deltaTime은 언제 계산될까? (2) | 2023.07.15 |
ScriptableObject를 저장하는 법: EditorUtility.SetDirty(Object target) (4) | 2021.03.17 |
댓글