Posted at

配列とNativeArrayとポインタとSystem.Runtime.CompilerServices.Unsafeによるfor性能比較

年末ですので初投稿です。……少なくともC#カテゴリでは初投稿です。


前提環境


  • Unity 2018.3.0f2

  • C# 7.3

  • API Compatibility Level .NET Standard 2.0


TL;DR

for文で回しながらアクセスするなら最速はポインタってはっきりわかんだね。


NativeArray<T> where T : unmanagedについて

Unity 2018.1から登場した新機能です。

NativeArray<T>はガベージコレクションの対象ではない、Unityのネイティブランタイム層でメモリアロケーションされた配列です。

NativeArray<T>は構造体であり、ジェネリック型引数Tとして値型を取ります。Tは更にBlittable型でなくてはなりません。

NativeArrayを使用する際のメリットとして以下が挙げられます。


  • IL2CPPビルドで配列よりも高速に動作する

  • いくらアロケートしてもガベージコレクタが動き出さない

  • Burst CompilerによってC# Job Systemにおいて特殊な最適化が掛けられて爆速に動作する(らしい)


テストコード

「1万要素のint型配列・NativeArrayを用意して、それに順次書き込みをする」を1万回実行してその累計時間をSystem.Diagnostics.Stopwatchクラスで計測しました。

厳密な事を言えば配列を確保する部分をStopwatchで計測する時間外に除けた方が良いです。

using System.Diagnostics;

using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.UI;
using Unity.Collections;

public sealed class TestManager : Button
{
public Text NativeArray;
public Text Array;
public Text UnsafeArray;
public Text Ptr;
public Text UnsafePtr;
private const int Length = 10000;
Stopwatch sp;

// Start is called before the first frame update
protected override void Start()
{
base.Start();
sp = new Stopwatch();
NativeArray = GameObject.Find(nameof(NativeArray)).GetComponent<Text>();
Array = GameObject.Find(nameof(Array)).GetComponent<Text>();
UnsafeArray = GameObject.Find(nameof(UnsafeArray)).GetComponent<Text>();
Ptr = GameObject.Find(nameof(Ptr)).GetComponent<Text>();
UnsafePtr = GameObject.Find(nameof(UnsafePtr)).GetComponent<Text>();
}

void NativeArrayTest()
{
var array = new NativeArray<int>(Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int j = 0; j < Length; j++)
for (int i = 0; i < array.Length; i++)
array[i] = i;
array.Dispose();
}

void UnsafeArrayTest()
{
var array = new int[Length];
for (int j = 0; j < Length; j++)
{
ref var item = ref array[0];
for (int i = 0; i < Length; i++, item = ref Unsafe.Add(ref item, 1))
item = i;
}
}

void ArrayTest()
{
var array = new int[Length];
for (int j = 0; j < Length; j++)
for (int i = 0; i < array.Length; i++)
array[i] = i;
}

unsafe void UnsafePtrTest()
{
var ptr = stackalloc int[Length];
for (int j = 0; j < Length; j++)
{
ref var item = ref ptr[0];
for (int i = 0; i < Length; i++, item = ref Unsafe.Add(ref item, 1))
item = i;
}
}

unsafe void PtrTest()
{
var ptr = stackalloc int[Length];
var start = ptr;
for (int j = 0; j < Length; j++)
{
ptr = start;
for (int i = 0; i < Length; i++, ptr++)
*ptr = i;
}
}

public override void OnPointerClick(UnityEngine.EventSystems.PointerEventData eventData)
{
base.OnPointerClick(eventData);
sp.Reset();
sp.Start();
NativeArrayTest();
sp.Stop();
NativeArray.text = nameof(NativeArray) + " : " + sp.ElapsedMilliseconds.ToString();
sp.Reset();
sp.Start();
ArrayTest();
sp.Stop();
Array.text = nameof(Array) + " : " + sp.ElapsedMilliseconds.ToString();
sp.Reset();
sp.Start();
UnsafeArrayTest();
sp.Stop();
UnsafeArray.text = nameof(UnsafeArray) + " : " + sp.ElapsedMilliseconds.ToString();
sp.Reset();
sp.Start();
PtrTest();
sp.Stop();
Ptr.text = nameof(Ptr) + " : " + sp.ElapsedMilliseconds.ToString();
sp.Reset();
sp.Start();
UnsafePtrTest();
sp.Stop();
UnsafePtr.text = nameof(UnsafePtr) + " : " + sp.ElapsedMilliseconds.ToString();
}
}


結果

平均実行時間ms(10000回)


NativeArray<int>
int[]
Unsafe int[]
stackalloc int[]
Unsafe stackalloc int[]

Editorビルド
4074
344
1109
361
1106

Monoビルド
99
79
92
43
97

IL2CPPビルド
68
103
219
47
203

Editorでの実行結果は配布するプロダクトと無関係なので参考程度に考えてください。

Monoビルドでは.NETのマネージドな配列を扱う方がNativeArray<T>より高速に動作します。

IL2CPPビルドではマネージド配列よりもNativeArray<T>の方が高速です。

実験してみて驚いたことはIL2CPPビルドでのマネージド配列がMonoのそれより明らかに低速なことでした。

何故なのでしょうかね?

System.Runtime.CompilerServices.Unsafeクラスによる擬似ポインタはEditor環境以外では他の手段の倍以上遅かったので、ジェネリックなポインタを使用したいという強い動機がない限り使わない方が良いでしょう。

最速はポインタってはっきりわかんだね。


ref T System.Runtime.CompilerServices.Unsafe.Add<T>(ref T value, int offset)について

C# 7から導入された言語機能である参照戻り値と参照ローカル変数、C# 7.3から追加された機能であるref再代入を活用することでジェネリック型のポインタのようなものを扱うことが可能です。

System.Runtime.CompilerServices.Unsafeクラスは参照をひたすら暗黒魔術でポインタの如く扱うことに特化したクラスです。

Addメソッド=ポインタの加減算

Asメソッド=ポインタの型変換

と解釈できます。

C#ではポインタの型にジェネリック型を指定できないため、ジェネリックなポインタを扱いたいならばUnsafeクラスを利用しましょう。

このクラスを活用した例としてはSystem.Span<T>構造体が挙げられます。