2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity,C#】NativeContainerの速度検証

Last updated at Posted at 2025-04-07

はじめに

今回はUnityで使用できるコレクションである NativeContainer について速度計測します。
その NativeContainer とはUnityが提供する Unity.Collections あるいは Unity.Collections.LowLevel.Unsafe 名前空間に属するコレクションのことです。
これらはアンマネージドメモリを使用するように実装してあります。
そのため確保したメモリがGC対象になりませんが、手ずから解放してやらないとメモリリークの原因になるという側面もあります。
NativeContainer の中では、Jobシステムに使用される NativeArray が有名でしょうか。
しかし実は、配列やリスト、Dictionaryなどのおなじみのコレクションも再現されています。
そこで今回はこれらのランダムアクセスや連続参照、要素の追加/削除の速度を見てみようと思います。
もしAllocationなしで高速なListやDictionaryが使えるなら素晴らしいですからね。

ちなみに、一応 NativeArray の中身について触れているので、参考までに前回の記事を見てもいいかもしれません。(見てほしいだけ)

検証内容について

今回の調査対象は以下の NativeContainer です。
これらは公式ドキュメントに記載された物から選抜しました。

  • NativeArray 配列に近いコレクション。
  • NativeList Listに近いコレクション。
  • UnsafeListList Listに近いコレクション。
  • NativeHashMap Dictionaryに近いコレクション。
  • UnsafeNativeHashMap Dictionaryに近いコレクション。
  • ParallelNativeHashMap Dictionaryに近いコレクション。

また、比較用に以下の一般コレクションさんにも参加ねがいます。

  • 配列
  • Span
  • List
  • Dictionary

そしてそれぞれに対して実施する検証内容の一覧が以下です。

コレクション名 連続参照計測 連続参照計測(Job) 要素追加+削除計測
NativeArray
NativeList
UnsafeListList
NativeHashMap
UnsafeNativeHashMap
ParallelNativeHashMap
配列
Span
List
Dictionary

なお、連続参照計測は連続参照計測(Job)と同時に行い、NativeContainerでJobシステムを使用した際のスコアを通常の参照時のスコアと同時に比較することにします。

補足・ParallelとUnsafeってなに?

検証前に、先述のNativeContainerたちのコレクション名についた、ParallelやらUnsafeが何を意味するのかについて少し触れます。
まず、Parallelは並列読み取り、書き込みが可能になっているデータ型のようです。
これは IJobParallelFor のようなマルチスレッドのJobシステムに対応した物でしょう。
例えば並列書き込みの場合 AsParallellWriter() メソッドによりParallellWriter 構造体という型で書き込み用のコピーを作成するようです。

次にUnsafeについてですが、これはドキュメントを見ただけではさっぱりでした。
ですのでコードを確認したところ、単に参照時にメモリ範囲の安全性チェックすべきところをすっ飛ばしているだけの様子です。
これは Enumerator.MoveNext() を比較するとすぐに分かります。

NativeArrayやNativeListのMoveNext(展開)
NativeArray.cs
    public unsafe bool MoveNext()
    {
    m_Index++;
    if ( m_Index < m_Array.m_Length )
    {
       AtomicSafetyHandle.CheckReadAndThrow(m_Array.m_Safety);
       value = UnsafeUtility.ReadArrayElement<T>(m_Array.m_Buffer, m_Index);
       return true;
    }

    value = default(T);
    return false;
    }
UnsafeListのMoveNext(展開)
UnsafeList.cs
    public Enumerator GetEnumerator()
    {
    return new Enumerator { m_Ptr = Ptr, m_Length = Length, m_Index = -1 };
    }
UnsafeList.cs
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool MoveNext() => ++m_Index < m_Length;

これが何をしているのかというと、C#のUnsafeコードの例ではよく見るポインタをスライドするだけの連続参照処理ですね。
となると、なんだかUnsafe系のNativeContainerは速そうです。
期待しましょう。

検証

1.検証方法について

まず検証方法について説明します。
NativeArray の性能を知るために、私は以下の環境で検証を行います。

  • テスト環境はUnityのユニットテスト環境であるTestRunerとPerformance Testing Extension。
  • Unity(ver 6000.0.35f1)で実行。

また、テストコードのサンプルは以下です。
まずは連続参照テストからご覧ください。

連続参照テストコードのイメージ(展開)
NativeContainerPaformanceTest.cs
    Measure.Method(() =>
    {
        long sum = 0;
        for ( int i = 0; i < unsafeList.Length; i++ )
        {
        sum += unsafeList[i];
        }
        // 記録用フィールド変数に記録。
        // コンパイラの過剰最適化予防を兼ねて値を使用し、最後に合計値の精度チェックに使用。
        unsafeListSum = sum;
    })
    .WarmupCount(WarmupCount)
    .MeasurementCount(MeasurementCount)
    .IterationsPerMeasurement(IterationsPerMeasurement)
    .Run();

続いて同時に行う連続参照テスト(Job)のコードです。

連続参照テスト(Job)で使用するJob構造体のイメージ(展開)
NativeContainerPaformanceTest.cs
    private struct NativeListSumJob : IJob
    {
    [ReadOnly] public NativeList<int> Input;
    public NativeArray<long> Result;

    public void Execute()
    {
        long sum = 0;
        for ( int i = 0; i < Input.Length; i++ )
        {
        sum += Input[i];
        }
        Result[0] = sum;
    }
    }

このテストで分かることは各コレクションの連続アクセスの速度です。
そして NativeContainer は多くの場合JobSystemと共に用いられるものであることから、JobSystem(IJobForとIJobParallelForの二通り)で同じ操作のテストも行います。

最後に値の追加・削除テストのコードサンプルは以下です。

要素追加・削除のテストコードのイメージ(展開)
NativeContainerAddRemoveTest.cs

    Measure.Method(() =>
    {
        // テスト用にデータを準備
        unsafeList.Clear();
        for ( int i = 0; i < ElementCount; i++ )
        {
        unsafeList.Add(i);
        }

        // リストが空になるまで最後の要素を削除し続ける
        while ( unsafeList.Length > 0 )
        {
        unsafeList.RemoveAt(unsafeList.Length - 1);
        }

        // 確認(測定後)
        if ( unsafeList.Length != 0 )
        {
        Debug.LogError($"UnsafeList still has {unsafeList.Length} elements after removal");
        }
    })
    .WarmupCount(WarmupCount)
    .MeasurementCount(MeasurementCount)
    .IterationsPerMeasurement(IterationsPerMeasurement)
    .Run();

それでは実際に検証してみます。

2.検証結果

前項の条件でテストした結果が以下になります。
まずは連続アクセスから。

連続アクセス検証結果

UnsafeListさんはやーーーーーーーーーい!!!!
これ、Median(中央値)が小さい=速い順にソートしているので上にあるほど速いです。
そしてUnsafeListさん、JobSystemを使っても使わなくても全コレクションで一位ですよ。
これは使わない手はありませんね!
AllocationなしのコレクションがSpanをぶっちぎってるって中々ステキです。
もちろん安全ではないのでしょうが、運用したくなるロマンがあります。
Jobシステムなしでも使う価値感じます。
そして他に気になるのはやはり NativeHashMap さんでしょうか。
やはり遅いといえば遅いのですが、通常のDictionaryの二割弱、全然連続アクセスで使えるレベルになっていますね。(Jobシステム込みなら)
なぜかUnsafeのHashMapの方が遅いのは不思議ですが、NativeContainerのHashMap族にはなにかしら可能性感じます。

ちなみにテスト時の処理結果はJob系含めて全部正常で、精度に問題があるテストケースは存在しませんでした。

要素の追加・削除検証結果

UnsafeList様はやーーーーーーーーーーーい!!!!
ここでも一位、にくいヤツです。
そしてDictionary系の話もすると、NativeHashMapとDictionaryはほとんど同じみたいですね。
でもUnsafeHashMapがまた不甲斐ない感じになっているのは不思議です。
多分使わないのでかなりあと回しにはなりそうですが、いつか中身見て見たいですね。

終わりに

結果としてとんだ UnsafeListList 様PR記事になってしまいました。
でもこんなに速くてかっこいいんだから仕方ないですね。
みなさんもぜひ UnsafeListList 様使いましょう。

それでは今回の記事は終わりです。
ご意見やご指摘をいただけると嬉しいので、良ければコメントお願いします。
周りに技術のことを聞いたり相談できる人がいないので、たまに来るコメントの鋭いご指摘が大変貴重な栄養になるので……。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?