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


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

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

https://qiita.com/cushionA/items/ff31e6266815dc9258c7

# 検証内容について

今回の調査対象は以下の **```NativeContainer```** です。
これらは[公式ドキュメント](https://docs.unity3d.com/Packages/com.unity.collections@2.6/manual/collection-types.html)に記載された物から選抜しました。

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

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

- **配列**
- **```Span```**
- **```List```**
- **```Dictionary```**

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

<table>
  <caption></caption>
  <thead>
    <tr>
      <th><strong>コレクション名</strong></th>
      <th><strong>連続参照計測</strong></th>
      <th><strong>連続参照計測（Job）</strong></th>
      <th><strong>要素追加＋削除計測</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>NativeArray</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong></strong></td>
    </tr>
    <tr>
      <td><strong>NativeList</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>UnsafeListList</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>NativeHashMap</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>UnsafeNativeHashMap</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>ParallelNativeHashMap</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>配列</strong></td>
      <td><strong>〇</strong></td>
      <td><strong></strong></td>
      <td><strong></strong></td>
    </tr>
    <tr>
      <td><strong>Span</strong></td>
      <td><strong>〇</strong></td>
      <td><strong></strong></td>
      <td><strong></strong></td>
    </tr>
    <tr>
      <td><strong>List</strong></td>
      <td><strong>〇</strong></td>
      <td><strong></strong></td>
      <td><strong>〇</strong></td>
    </tr>
    <tr>
      <td><strong>Dictionary</strong></td>
      <td><strong>〇</strong></td>
      <td><strong></strong></td>
      <td><strong>〇</strong></td>
    </tr>
  </tbody>
</table>

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

# 補足・ParallelとUnsafeってなに？
検証前に、先述のNativeContainerたちのコレクション名についた、ParallelやらUnsafeが何を意味するのかについて少し触れます。
まず、Parallelは並列読み取り、書き込みが可能になっているデータ型のようです。
これは **```IJobParallelFor```** のようなマルチスレッドのJobシステムに対応した物でしょう。
例えば並列書き込みの場合 **```AsParallellWriter()```** メソッドにより[ParallellWriter](https://docs.unity3d.com/Packages/com.unity.collections@2.6/api/Unity.Collections.NativeParallellNativeHashMap-2.ParallellWriter.html) 構造体という型で書き込み用のコピーを作成するようです。

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

<!-- open属性なし -->
<details><summary><strong>NativeArrayやNativeListのMoveNext(展開)</strong></summary>

```C#: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;
    }
```
</details>

<!-- open属性なし -->
<details><summary><strong>UnsafeListのMoveNext(展開)</strong></summary>

```C#:UnsafeList.cs
    public Enumerator GetEnumerator()
    {
    return new Enumerator { m_Ptr = Ptr, m_Length = Length, m_Index = -1 };
    }
```

```C#:UnsafeList.cs
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool MoveNext() => ++m_Index < m_Length;
```

</details>

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

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

- **テスト環境はUnityのユニットテスト環境であるTestRunerとPerformance Testing Extension。**
- **Unity（ver 6000.0.35f1）で実行。**

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

<!-- open属性なし -->
<details><summary><strong>連続参照テストコードのイメージ(展開)</strong></summary>

```C#: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();
```
</details>

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

<!-- open属性なし -->
<details><summary><strong>連続参照テスト(Job)で使用するJob構造体のイメージ(展開)</strong></summary>

```C#: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;
    }
    }
```
</details>


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

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

<!-- open属性なし -->
<details><summary><strong>要素追加・削除のテストコードのイメージ(展開)</strong></summary>

```C#: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();
```
</details>

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

## 2.検証結果

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

|**連続アクセス検証結果**|
|:-:|
|![](https://github.com/cushionA/ImageStrage/blob/main/NativeContainer%E8%A8%88%E6%B8%AC/%E9%80%A3%E7%B6%9A%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%86%E3%82%B9%E3%83%88.png?raw=true)|

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

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

|**要素の追加・削除検証結果**|
|:-:|
|![](https://github.com/cushionA/ImageStrage/blob/main/NativeContainer%E8%A8%88%E6%B8%AC/%E8%BF%BD%E5%8A%A0%E5%89%8A%E9%99%A4%E3%83%86%E3%82%B9%E3%83%88.png?raw=true)|

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

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

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

# 要素数が少ない場合の追加検証(2025/4/16)
Jobシステムは要素数が少ないと逆に不利になる……と聞きまして、確かにありそうだと感じて再検証をしました。
今度は50~100の要素数で確認です。
結果は以下。

| **要素数100での検証（上から順に遅い）**|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/NativeContainer%E8%A8%88%E6%B8%AC/%E8%A6%81%E7%B4%A0%E6%95%B0100.png?raw=true)|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/NativeContainer%E8%A8%88%E6%B8%AC/%E8%A6%81%E7%B4%A0%E6%95%B0100%E3%82%B0%E3%83%A9%E3%83%95.png?raw=true)|

今回は数値の単位が小さすぎたので、出力した平均値からグラフも作成しました。
上から順に遅いので、明らかにJob関連は遅いですね。
要素数50でも似たような結果に落ち着きました。
しかしNativeContainerの優位は揺らがないと分かったのが唯一の救いでしょうか。

でも今ちょうど、Jobシステムを活用したゲームAIをウンウン悩みながら作っていたところだったのでかなり残念です。
まぁ普通のゲームの敵なんていても数十ですからね。
悲しいですがせっかくAI作ってるので、できたらちゃんと検証はしたいと思います。
Job関連の設定が悪い可能性もありそうですし、今作ってるAIでは色々とJob関連の最適化を頑張ってるのであるいは……。