📊

【Unity】ArrayPool<T> vs NativeArray<T>(Allocator.Temp)

2024/11/26に公開

Unityでライブラリを作成している際にちょっと気になったので。

はじめに

C#では、一時的なバッファとして利用する配列を借りるためのクラスとしてSystem.Buffers.ArrayPool<T>が提供されています。

// バッファをプールから借りる
var buffer = ArrayPool<int>.Shared.Rent(bufferSize);
try
{
    // Spanにして適当なメソッドに渡す
    Write(buffer.AsSpan(0, bufferSize));
}
finally
{
    // 使用後はプールに返却
    ArrayPool<int>.Shared.Return(buffer);
}

一方、Unityにはネイティブメモリを手軽に利用するための構造体としてNativeArray<T>が用意されています。このNativeArrayにはメモリ確保に利用するAllocatorを指定することができ、これにAllocator.Tempを指定することで一時的に利用するメモリを高速に確保することが可能です。

// Allocator.TempでNativeArrayを作成
// 初期化が不要な場合はNativeArrayOptions.UninitializedMemoryを指定する
var buffer = new NativeArray<int>(bufferSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);

// Spanにして適当なメソッドに渡す
Write(buffer);

// 通常NativeArrayは明示的にDisposeしないとメモリリークを起こす
// ただし、Allocator.Tempに限りフレームの終了時に自動で解放される (Disposeを呼んでも何も起こらない)
// buffer.Dispose()

ではUnityで一時的なバッファを使いたい場合、どちらを使うのが効率的なんでしょうか...?

ベンチマーク

実際に計測してみましょう。実際にこんな感じのコードを書いてベンチマークを走らせてみます。

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using Unity.Collections;
using Unity.PerformanceTesting;

public class BufferBenchmark
{
    const int WarmupCount = 10;
    const int MeasurementCount = 1000;
    const int IterationsPerMeasurement = 100;

    [Test]
    [Performance]
    public void ArrayPool_128()
    {
        MeasureArrayPool(128);
    }

    [Test]
    [Performance]
    public void NativeArray_128()
    {
        MeasureNativeArray(128);
    }

    [Test]
    [Performance]
    public void ArrayPool_512()
    {
        MeasureArrayPool(512);
    }

    [Test]
    [Performance]
    public void NativeArray_512()
    {
        MeasureNativeArray(512);
    }

    [Test]
    [Performance]
    public void ArrayPool_4096()
    {
        MeasureArrayPool(4096);
    }

    [Test]
    [Performance]
    public void NativeArray_4096()
    {
        MeasureNativeArray(4096);
    }

    void MeasureArrayPool(int bufferSize)
    {
        Measure.Method(() =>
        {
            var buffer = ArrayPool<int>.Shared.Rent(bufferSize);
            try
            {
                Write(buffer.AsSpan(0, bufferSize));
            }
            finally
            {
                ArrayPool<int>.Shared.Return(buffer);
            }
        })
        .SampleGroup(new SampleGroup("Time", SampleUnit.Nanosecond))
        .WarmupCount(WarmupCount)
        .MeasurementCount(MeasurementCount)
        .IterationsPerMeasurement(IterationsPerMeasurement)
        .Run();
    }

    void MeasureNativeArray(int bufferSize)
    {
        Measure.Method(() =>
        {
            var buffer = new NativeArray<int>(bufferSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
            Write(buffer);
        })
        .SampleGroup(new SampleGroup("Time", SampleUnit.Nanosecond))
        .WarmupCount(WarmupCount)
        .MeasurementCount(MeasurementCount)
        .IterationsPerMeasurement(IterationsPerMeasurement)
        .Run();
    }

    // 適当に渡されたバッファに数字を書き込みだけのメソッド
    static void Write(Span<int> buffer)
    {
        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = i;
        }
    }
}

結果は以下の通り。

バッファサイズが小さい場合はNativeArrayの方が高速ですが、大きめの場合はArrayPoolの方に軍配が上がる、といった感じですね。

とはいえそこまで大きな差ではないので、どちらを選択しても問題ないでしょう。手動で後始末をする必要がないNativeArray<T>(Allocator.Temp)の方が扱いやすくはありますが、ArrayPoolの方がC#erとして馴染み深いかもしれません。いずれにせよ、好みで選んで良いと思います。

結論

  • バッファサイズが小さい場合はNativeArray、大きめの場合はArrayPoolが高速
  • ただし差としては十分小さいため、好みで選んでよし

Discussion