はじめに
今回は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(展開)
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(展開)
public Enumerator GetEnumerator()
{
return new Enumerator { m_Ptr = Ptr, m_Length = Length, m_Index = -1 };
}
[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)で実行。
また、テストコードのサンプルは以下です。
まずは連続参照テストからご覧ください。
連続参照テストコードのイメージ(展開)
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構造体のイメージ(展開)
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の二通り)で同じ操作のテストも行います。
最後に値の追加・削除テストのコードサンプルは以下です。
要素追加・削除のテストコードのイメージ(展開)
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
様使いましょう。
それでは今回の記事は終わりです。
ご意見やご指摘をいただけると嬉しいので、良ければコメントお願いします。
周りに技術のことを聞いたり相談できる人がいないので、たまに来るコメントの鋭いご指摘が大変貴重な栄養になるので……。