配列のメモリアロケーションを抑える

最終更新日


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>();
    }
}

このサンプルは非常に簡単なものになっていますが、ちゃんとした実装はListOfOftenOneCompactListがわかりやすいです。

実装次第で3つまでアロケーションしないようにしたり、ListではなくDictionaryとして使ったりなど応用が利きます。


コメントを残す

メールアドレスが公開されることはありません。

コメントする