-
Unity BurstCompiler2023년 10월 30일
- 유니얼
-
작성자
-
2023.10.30.:53
728x90Unity의 BurstCompiler는 고성능 C# 코드를 만들기 위한 오픈 소스 컴파일러입니다. 이 글에서는 BurstCompiler에 대한 설명과 그 도움되는 이유, 동작 방식, 사용 방법, 성능 향상 등에 대해 알아보겠습니다.
Burst Compiler란 무엇인가?
Burst Compiler는 Unity의 기능으로서, C# 코드를 네이티브 어셈블리 코드로 변환하여 실행 속도를 향상시킵니다. Burst Compiler는 분석과 최적화를 통해 코드를 빠르게 실행할 수 있는 방식으로 변환합니다. Unity에서 제공하는 기본 컴파일러인 Mono로 작성된 C# 코드를 최적화하고, SIMD(Single Instruction, Multiple Data) 연산을 이용하여 병렬 처리 성능을 향상시킵니다.
https://blog.unity.com/technology/enhancing-mobile-performance-with-the-burst-compiler
https://docs.unity3d.com/Packages/com.unity.burst@0.2/manual/index.html
Burst Compiler의 이점
Burst Compiler를 사용하면 Unity에서 C# 코드를 더 빠르게 실행할 수 있습니다. 그 이유는 Burst Compiler가 네이티브 어셈블리 코드로 변환되기 때문입니다. 이로써 프로세스와 연산의 효율성이 향상되어 게임의 성능을 향상시킬 수 있습니다. Burst Compiler는 또한 SIMD 연산을 지원하여 병렬 처리 능력을 향상시킵니다.
- 플랫폼 최적화: 버스트 컴파일러는 플랫폼별 기계어 코드를 생성하여 대상 플랫폼에 맞게 코드를 조정합니다. 이는 귀하의 코드가 다양한 CPU의 고유한 기능을 최대한 활용할 수 있음을 의미합니다.
- 자동 벡터화: 컴파일러는 코드를 자동으로 벡터화하여 적절한 경우 SIMD(단일 명령, 다중 데이터) 작업을 수행할 수 있습니다. 이는 물리 시뮬레이션과 같은 고성능 컴퓨팅 작업에 매우 중요합니다.
- 가비지 수집 없음: 버스트 컴파일된 코드는 가비지 수집을 피하거나 최소화하여 플레이어의 경험을 저하시킬 수 있는 프레임 속도 스파이크를 줄일 수 있습니다.
- 관리 코드: C++와 같은 하위 수준 언어보다 개발자 친화적인 C#으로 코드를 작성합니다. 버스트 컴파일러는 효율적인 기계어 코드로의 변환을 처리합니다.
IL2CPP(Intermediate Lacguage to CPP)
기존 유니티 컴파일 프로세스
이러한 과정을 거쳐서 C# 코드를 컴퓨터나 모바일에서 동작이 가능하도록 코드를 변환합니다. 하지만 요즘 컴파일 과정은 코드 변환 뿐만이 아니라 최적화도 해줍니다. Unity의 IL2CPP(Intermediate Language to C++) 컴파일은 강력한 도구이지만 대규모 게임에 적용하면 다음과 같은 주목할만한 과제가 발생합니다.
- 빌드 시간 증가: 대규모 프로젝트에서는 빌드 시간이 상당히 길어져 반복 속도에 영향을 미칩니다.
- 더 큰 바이너리 크기: IL2CPP 생성 코드로 인해 더 큰 게임 바이너리가 생성되는데, 이는 저장 공간이 제한된 플랫폼의 문제입니다.
- 성능 최적화: 대규모 게임에서 최고의 성능을 달성하려면 시간이 많이 걸릴 수 있으며 광범위한 프로파일링 및 최적화 노력이 필요할 수 있습니다.
이러한 이슈로 대용량 데이터를 빠르게 처리하기 위해서 현재 대부분의 CPU에서는 SIMD(Single Instruction Multiple Data)를 지원하고 있습니다.
SIMD(Single Instruction Multiple Data)
SIMD는 Single Instruction, Multiple Data의 약자로, 한 번의 명령으로 여러 데이터를 동시에 처리할 수 있는 기술입니다. 이를 통해 단일 명령을 여러 데이터 포인트에 동시에 적용할 수 있으므로 병렬화가 가능한 작업의 성능이 크게 향상될 수 있습니다. Burst Compiler는 SIMD를 활용하여 C# 코드를 병렬 처리하기 때문에, 성능 향상을 기대할 수 있습니다. 예를 들어 Vector4에 대한 연산을 기준으로 설명을 한다면 다음 그림으로 설명할 수 있습니다.
Unity의 버스트 컴파일러는 이러한 SIMD에 대한 최적화를 진행을 해줍니다. SIMD 지침을 최대한 활용하여 특정 작업을 최적화하고 가속화하도록 설계되었습니다.
Burst Compiler 프로세스
Burst Compiler는 C# 코드를 네이티브 어셈블리 코드로 변환하여 실행합니다. 이 과정에서 JIT(Just-in-Time) 컴파일이 아닌 AOT(Ahead-of-Time) 컴파일을 사용합니다. 따라서, 어셈블리 코드가 이미 컴파일되어 있기 때문에 실행 시에 추가적인 컴파일 단계가 필요하지 않습니다. Burst Compiler는 코드 분석과 최적화를 통해 실행 속도를 극대화합니다.
BurstCompiler의 장점
1, 성능 향상:
특정 유형의 계산에 대해 놀라운 성능 향상을 달성할 수 있습니다. 물리 시뮬레이션, AI 경로 찾기, 렌더링과 같은 작업에는 대규모 데이터 세트에 대한 광범위한 계산이 포함되는 경우가 많습니다. 이러한 데이터세트를 더 효율적으로 처리할 수 있어 속도가 크게 향상됩니다.
2, 교차 플랫폼 최적화:
Unity는 PC와 콘솔부터 모바일 장치까지 다양한 플랫폼을 지원합니다. 버스트 컴파일러는 플랫폼에 구애받지 않는 성능 향상 방법을 제공합니다. Burst Compiler는 SIMD 가속 코드를 생성함으로써 플랫폼별 최적화 없이도 게임이 다양한 하드웨어에서 효율적으로 실행될 수 있도록 보장합니다.
3, 데이터 중심 디자인:
버스트 컴파일러는 Unity가 장려하는 DOD(데이터 지향 디자인) 원칙과 잘 맞습니다. DOD는 최적의 메모리 레이아웃과 효율적인 처리를 위해 데이터 구조화를 권장합니다. 버스트 컴파일러는 이러한 철학에 완벽하게 부합하므로 개발자는 고도로 최적화된 데이터 기반 시스템을 만들 수 있습니다.
BurstCompiler의 단점
Unity의 버스트 컴파일러는 C# 코드를 최적화하고 런타임 성능을 향상시키는 강력한 도구이지만 어려움이 없는 것은 아닙니다.
1, 수동 메모리 관리:
버스트 컴파일러를 사용할 때 가장 주목할만한 과제 중 하나는 수동 메모리 관리의 필요성입니다. 메모리 할당 및 할당 해제(가비지 수집)를 자동으로 처리하는 C#과 같은 고급 언어와 달리 버스트 최적화 코드에서는 개발자가 메모리를 명시적으로 관리해야 합니다.
2, 디버깅 복잡성:
버스트 컴파일된 코드를 디버깅하는 것은 어려운 작업이 될 수 있습니다. 버스트 컴파일러가 적용한 최적화로 인해 원래 C# 소스 코드와 직접적인 상관관계가 없는 어셈블리 코드가 생성되는 경우가 많습니다. 이러한 연결 끊김으로 인해 디버깅이 복잡해질 수 있습니다.
3, 코드 작성의 복잡성:
버스트 컴파일러의 이점을 최대한 활용하려면 개발자는 최적화 요구 사항을 준수하는 코드를 작성해야 합니다. 이로 인해 코드베이스가 복잡해질 수 있습니다. 다음은 BurstCompiler에서 지원하는 변수 타입입니다.
변수 지원 여부 Primitive types - bool
- char
- sbyte/byte
- short/ushort
- int/uint
- long/ulong
- float
- double
지원 Primitive types - String
- decimal
지원하지 않음Vector types - bool2/ bool3/bool4
- uint2/ uint3/uint4
- int2/ int3/int4
- float2/ float3/float4
지원 Enum types 지원 Struct types 지원 Pointer types 지원 Generic types 구조체와 함께 사용되는 일반 유형을 지원합니다.(Class는 지원하지 않습니다.) Array types 관리형 어레이는 버스트에서 지원되지 않습니다. 예를 들어 대신 기본 컨테이너를 사용해야 합니다 NativeArray<T>. Unity의 버스트 컴파일러는 계산 집약적인 작업의 성능을 크게 향상시킬 수 있지만 이로 인해 발생하는 문제를 인식하는 것이 중요합니다. 수동 메모리 관리, 디버깅 복잡성, 코드 작성의 복잡성은 버스트 컴파일러를 프로젝트에 통합할지 여부를 결정할 때 주요 고려 사항 중 하나입니다.
BurstCompiler 예시
기존 Unity 코드
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DefaultObjectSpawner : MonoBehaviour { public GameObject objectPrefab; // Prefab of the object to spawn public int numberOfObjects = 20000; // Number of objects to spawn private Transform[] spawnedObjects; private void Start() { spawnedObjects = new Transform[numberOfObjects]; // Spawn objects at random positions within the specified range for (int i = 0; i < numberOfObjects; i++) { Vector3 spawnPosition = new Vector3(Random.Range(-50f, 50f), Random.Range(-50f, 50f), Random.Range(-50f, 50f)); spawnedObjects[i] = Instantiate(objectPrefab, spawnPosition, Quaternion.identity).transform; } } private void Update() { // Move and reset objects when they exceed a z-coordinate of 50 for (int i = 0; i < numberOfObjects; i++) { Transform obj = spawnedObjects[i]; Vector3 newPosition = obj.position + Vector3.forward * Time.deltaTime; if (newPosition.z > 50f) { // Reset the object's position to (0, 0, -50) obj.position = new Vector3(0f, 0f, -50f); } else { obj.position = newPosition; } } } }
실행 결과
BurstCompiler 적용한 코드
using System; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using UnityEngine; using Random = UnityEngine.Random; public class BurstObjectSpawner : MonoBehaviour { public GameObject objectPrefab; // Prefab of the object to spawn public int numberOfObjects = 20000; // Number of objects to spawn private Transform[] spawnedObjects; private NativeArray<Vector3> objectPositions; // NativeArray for object positions private void Start() { spawnedObjects = new Transform[numberOfObjects]; objectPositions = new NativeArray<Vector3>(numberOfObjects, Allocator.Persistent); // Spawn objects at random positions within the specified range for (int i = 0; i < numberOfObjects; i++) { Vector3 spawnPosition = new Vector3(Random.Range(-50f, 50f), Random.Range(-50f, 50f), Random.Range(-50f, 50f)); spawnedObjects[i] = Instantiate(objectPrefab, spawnPosition, Quaternion.identity).transform; objectPositions[i] = spawnPosition; } } private void Update() { // Perform Burst-compiled computation on object positions new MoveObjectsJob { objectPositions = objectPositions, deltaTime = Time.deltaTime }.Schedule(numberOfObjects, 64).Complete(); // Update object positions based on the computed values for (int i = 0; i < numberOfObjects; i++) { spawnedObjects[i].position = objectPositions[i]; } } private void OnDestroy() { // Dispose of the NativeArray when the script is destroyed objectPositions.Dispose(); } [BurstCompile] private struct MoveObjectsJob : IJobParallelFor { public NativeArray<Vector3> objectPositions; public float deltaTime; public void Execute(int index) { Vector3 newPosition = objectPositions[index] + Vector3.forward * deltaTime; if (newPosition.z > 50f) { // Reset the object's position to (0, 0, -50) objectPositions[index] = new Vector3(0f, 0f, -50f); } else { objectPositions[index] = newPosition; } } } }
실행 결과
마무리
Burst Compiler는 Unity 게임 개발에서 성능과 호환성 개선을 위해 많은 가치를 제공하는 도구입니다. 그러나 모든 상황에서 항상 최선의 선택은 아닙니다. 프로젝트 요구 사항과 목표에 따라 가장 적합한 방식과 도구를 선택하는 것이 중요합니다.
반응형다음글이전글이전 글이 없습니다.댓글