配列のメモリアロケーションを抑える
C#で配列を使う際にメモリアロケーションを抑えるテクニックを3つ紹介します。
stackalloc
stackallocを使うとヒープではなくスタックに載る配列を使えます。スタック上ならほとんどノーコストで使えます。
newの代わりにstackallocを使って確保し、Span型の変数に入れます。
Span<int> array = stackalloc int[10];
array[0] = 10;
スタック専用なのでclassやstructのフィールドには持てません。メソッド内のローカル変数として使います。より詳しい制限事項はref構造体の説明を見てください。
Unityでは2021.2から標準で使えます。それ以前のバージョンではSpanが存在しないのでSystem.Memory.dllを入れると使えるようになります。
オブジェクトプール
配列に限らず、ObjectPoolパターンを使えばメモリ確保の頻度を減らせます。特に配列では.NET標準でSystem.Buffers.ArrayPoolが用意されています。
ArrayPool.Shared.Rentで配列を取得し、使い終わったらArrayPool.Shared.Returnで返却します。すぐに返さなくてもいいのでStart()で取得してOnDestory()で返却することもできます。
const int arraySize = 10;
int[] array = ArrayPool<int>.Shared.Rent(arraySize);
array[0] = 10;
// 使い終わったら
ArrayPool<int>.Shared.Return(array);
配列の要素は0初期化されていないことがあるので注意してください。
また、配列のサイズも2の累乗になります。上のサンプルコードではサイズ10で取得していますが、取得された配列のサイズは16になっています。Lengthを取ったりforeachで扱うときには注意が必要です。
Frugal Object
FrugalObjectは要素が少ない内はstructのフィールドとして持っておき、要素が増えてからメモリ確保するパターンです。
次のサンプルコードでは、初めてAddされたときはフィールドのfirstに入れておき、2回目以降はlistにAddしています。こうすることで要素数が1つだけならメモリアロケーションは発生しません。
要素数1で済むことが多い箇所で使えばメモリアロケーションを減らせます。
public struct ListOfOftenOne<T>
{
private int count;
private T first;
private List<T> list;
public void Add(T value)
{
if (count == 0)
{
first = value;
}
else
{
InitializeList();
list.Add(value);
}
++count;
}
public T Get(int index)
{
if (count < 0 || count <= index)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
if (index == 0)
{
return first;
}
return list[index - 1];
}
private void InitializeList()
{
list ??= new List<T>();
}
}
このサンプルは非常に簡単なものになっていますが、ちゃんとした実装はListOfOftenOneやCompactListがわかりやすいです。
実装次第で3つまでアロケーションしないようにしたり、ListではなくDictionaryとして使ったりなど応用が利きます。