Unity

[Unity] JobSystem(2) NativeContainer와 TransformAccessArray

usingsystem 2024. 9. 6. 13:48
728x90

Job System과 Native Collections 특징

Native collections는 성능 최적화와 메모리 관리 효율성을 위해 제공하는 데이터 구조입니다. 네이티브 메모리라고 불리는 공유 메모리 타입에 저장됩니다.  메모리 관리되지 않는 포인터가 들어 있습니다. Job System과 함께 사용하면 잡이 복사본으로 작업하는 것이 아닌 메인 스레드와 공유되는 데이터에 액세스 할 수 있습니다.

주요 특징과 이점:

  • 멀티스레드 안전성: Native Collections은 Job System과 함께 사용할 때 쓰레드 안전성을 보장합니다. 일반적으로 C#의 List나 Array 같은 컬렉션은 쓰레드 안전하지 않아 멀티스레딩 작업에서 문제가 발생할 수 있습니다. 반면, Native Collections은 멀티스레딩 환경에서 안전하게 접근할 수 있습니다.
  • 고성능 데이터 접근: Native Collections은 메모리 관리를 수동으로 처리하고, 연속된 메모리 블록을 사용하여 캐시 효율성을 극대화합니다. Job System에서 이러한 구조의 데이터를 병렬로 처리하면 매우 빠른 성능을 얻을 수 있습니다.
  • 간편한 데이터 공유: Job System에서 작업 간 데이터를 공유할 때 Native Collections을 사용하면, 메모리 복사 없이도 데이터를 효율적으로 공유할 수 있습니다. Job과 Job 간의 데이터 의존성을 관리할 때 특히 유용합니다.

주의 사항

  • Dispose() 관리: Native Collections을 사용할 때는 수동으로 메모리를 해제해야 합니다. Job System에서 사용하는 경우에도 Job이 완료된 후 반드시 Dispose()를 호출하여 메모리 누수를 방지해야 합니다.
  • 데이터 동기화: Job System과 Native Collections을 사용할 때 데이터 의존성을 잘 관리해야 합니다. Job 간의 의존성을 관리하지 않으면 데이터 경합이나 오류가 발생할 수 있습니다. 이를 위해 JobHandle을 통해 작업 순서를 제어해야 합니다.

메모리 관리와 Dispose()

Native Collections는 C#런타임 가비지 컬렉터가 해당 컬렉션을 메모리를 관리하지 않는다. 그렇기 때문에 컬렉션이 더 이상 필요하지 않은 경우 직접 Dispose() 메소드를 호출하여 명시적으로 해제시켜야 한다.

  • 수동 메모리 관리: Native Collections은 관리되지 않는 메모리를 사용하므로, 사용 후 반드시 Dispose() 메서드를 호출하여 메모리를 해제해야 합니다.
  • Dispose()의 중요성: Dispose()를 호출하지 않으면 메모리 누수가 발생하며, 이는 성능 저하와 크래시의 원인이 됩니다.
  • 사용 패턴: 일반적으로 using 문을 사용하거나, 수동으로 Dispose()를 호출하여 메모리를 안전하게 관리합니다.

Allocator.Temp, Allocator.Persistent

Allocator.Temp와 Allocator.Persistent는 Unity에서 네이티브 컬렉션 메모리를 관리하는 방식이다.

외에도 Allocator.TempJob이나 WorldUpdateAllocator 등의 할당자가 있다.

 

Allocator.Temp

  • 용도: 단기 메모리 할당에 사용되며, 한 프레임 내에서만 유효합니다. 주로 임시 데이터나 짧은 수명의 데이터를 처리할 때 사용합니다.
  • 특징:
    • 매우 빠른 메모리 할당과 해제가 가능하여 성능에 큰 영향을 미치지 않습니다.
    • 메모리가 한 프레임 동안만 유효하기 때문에 프레임이 끝날 때 자동으로 해제됩니다.
    • 사용 시 주의할 점은 메모리를 한 프레임 이상 유지할 수 없으므로, 메모리 누수 방지를 위해 반드시 프레임 내에서 사용하고 끝내야 합니다.
  • 적합한 사용 사례: 임시 계산, 일회성 작업 등 빠르게 처리되고 바로 폐기될 데이터.

Allocator.Persistent

  • 용도: 장기 메모리 할당에 사용되며, 여러 프레임에 걸쳐 데이터를 유지해야 할 때 사용합니다.
  • 특징:
    • 메모리가 수동으로 해제될 때까지 유지되므로, 장기간 사용할 데이터를 처리하는 데 적합합니다.
    • 할당과 해제 속도가 상대적으로 느리지만, 데이터가 지속적으로 필요할 때는 이 점이 성능에 큰 영향을 미치지 않습니다.
    • 메모리를 사용한 후에는 반드시 해제를 해야 하며, 그렇지 않으면 메모리 누수가 발생할 수 있습니다.
  • 적합한 사용 사례: 게임 오브젝트 데이터, 지속적인 상태 정보, 장기간 사용할 데이터 구조 등.

NativeCollection 종류

NativeArray - NativeArray는 고정 크기의 배열입니다. 크기가 고정되어 있어 생성 시 설정한 크기를 변경할 수 없습니다.

NativeList - 크기 변경이 가능한 NativeArray입니다. 리스트로, 크기가 동적으로 변경될 수 있습니다.
NativeHashMap - 키-값 쌍을 저장하는 구조입니다.
NativeMultiHashMap - 한 키에 여러 개의 값을 할당할 수 있는 해시맵입니다.
NativeQueue - 선입선출(FIFO) 대기열입니다.

 

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public class JobSystemNativeArrayExample : MonoBehaviour
{
    private NativeArray<float> numbers;

    void Start()
    {
        // NativeArray 초기화
        numbers = new NativeArray<float>(1000, Allocator.Persistent);

        // Job 생성 및 실행
        VectorMultiplyJob job = new VectorMultiplyJob
        {
            data = numbers,
            multiplier = 2.5f // 각 요소에 곱할 값
        };

        // Job 스케줄링 (병렬 처리)
        JobHandle handle = job.Schedule(numbers.Length, 64); // 병렬로 처리할 데이터 길이와 배치 크기
        handle.Complete(); // Job 완료 대기

        // 결과 확인
        Debug.Log(numbers[0]);  // 첫 번째 요소 결과 출력

        // NativeArray 메모리 해제
        numbers.Dispose();
    }

    [BurstCompile]
    struct VectorMultiplyJob : IJobParallelFor
    {
        public NativeArray<float> data;
        public float multiplier;

        public void Execute(int index)
        {
            // 각 요소에 multiplier 곱하기
            data[index] *= multiplier;
        }
    }
}

Job System과 TransformAccessArray 특징

TransformAccessArray는 Transform 컴포넌트를 관리하는 배열로, Unity의 Jobs System에서 멀티스레딩으로 Transform을 조작할 수 있게 도와줍니다. 일반적으로 Transform은 메인 스레드에서만 접근이 가능하지만, 이 구조를 통해 병렬 처리 작업에서도 접근이 가능하도록 합니다.

using UnityEngine;
using UnityEngine.Jobs;
using Unity.Jobs;
using Unity.Burst;

public class TransformAccessArrayExample : MonoBehaviour
{
    private TransformAccessArray transformAccessArray;
    private MoveTransformsJob moveJob;
    private JobHandle jobHandle;

    public float moveSpeed = 1f;
    public GameObject[] gameObjects;

    void Start()
    {
        transformAccessArray = new TransformAccessArray(gameObjects.Length);

        foreach (var go in gameObjects)
        {
            transformAccessArray.Add(go.transform);
        }
    }

    void Update()
    {
        moveJob = new MoveTransformsJob
        {
            moveSpeed = moveSpeed,
            deltaTime = Time.deltaTime
        };

        jobHandle = moveJob.Schedule(transformAccessArray);
        jobHandle.Complete();
    }

    void OnDisable()
    {
        // 스크립트가 비활성화될 때 Dispose 호출하여 메모리 해제
        if (transformAccessArray.isCreated)
        {
            transformAccessArray.Dispose();
        }
    }

    [BurstCompile]
    struct MoveTransformsJob : IJobParallelForTransform
    {
        public float moveSpeed;
        public float deltaTime;

        public void Execute(int index, TransformAccess transform)
        {
            transform.position += Vector3.right * moveSpeed * deltaTime;
        }
    }
}

 

 

 

https://docs.unity3d.com/kr/2018.4/Manual/JobSystemNativeContainer.html

 

NativeContainer - Unity 매뉴얼

데이터 복사의 안전 시스템 프로세스에 대한 단점은 잡의 결과가 각 복사본 내에 격리된다는 것입니다. 이러한 제한을 극복하려면 결과를 NativeContainer라고 불리는 공유 메모리 타입에 저장해야

docs.unity3d.com

728x90