はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】stackallocを用いる事でGC.Allocを発生せずに配列を確保する

はじめに

今回はUnityでのstackallocの使用を想定して、簡単な説明記事を書きたいと思います。

メモリ領域について

Unityでは以下の大きく分けて3つのメモリ領域があります。

  • マネージメモリ: マネージヒープと ガベージコレクター を使用する制御されたメモリレイヤー。自動的にメモリ割り当てを行います。
  • C# アンマネージメモリ: Unity Collections 名前空間およびパッケージと組み合わせて使用できる、メモリ管理のレイヤーです。このメモリタイプは、メモリの未使用部分の管理に ガベージコレクター を使用しないため、“アンマネージ” と呼ばれています。
  • ネイティブメモリ: Unity がエンジンを実行するために使用する C++ メモリです。ほとんどの場合、このメモリには Unity ユーザーはアクセスできませんが、アプリケーションパフォーマンスのある面を微調整したい場合には、このメモリを知っておくと便利です。

Unity のメモリ - Unity マニュアル

Unityのメモリ

私は今までアンマネージメモリ = ネイティブメモリだと思っていましたが、一応言葉の定義としては別物として捉えた方が良さそうです。

またマネージドメモリには以下の3種類があります。

  • マネージヒープ: VM が ガベージコレクター (GC) を使って自動的に管理するメモリのセクションです。このため、マネージヒープ上に割り当てられたメモリは、GC Allocation (GC メモリ割り当て) と呼ばれます。Profiler は、このような割り当ての発生を GC.Alloc サンプルとして記録します。
  • スクリプトスタック: これは、アプリケーションがあらゆるコードスコープに出入りする際に構築され、開放されます。
  • ネイティブ VM メモリ: Unity のスクリプトレイヤーに関連するメモリが含まれています。ほとんどの場合、ネイティブ VM のメモリを操作する必要はありませんが、コードが生成する実行コードに関連するメモリが含まれていることを知っていると便利です。特に、ジェネリック の使用、リフレクション が使用するタイプのメタデータ、VM の実行に必要なメモリが含まれていることを知っていると役に立ちます。

マネージドヒープはガベージコレクション(GC)によって管理されているので、メモリを確保するとGC.Allocが発生します。

今回紹介するstackallocを利用するとデータはスタック領域に確保されます。
一応スクリプトスタックに該当するので合ってる?んですかね。(間違っていたら教えていただけると)

スタック領域を利用するとGC.Allocは発生しません。

stackallocとは

stackalloc 式を使用すると、スタックにメモリ ブロックを割り当てることができます。 メソッドの実行中に作成された、スタックに割り当てられたメモリ ブロックは、そのメソッドが戻るときに自動的に破棄されます。 stackalloc を使用して割り当てられたメモリを明示的に開放することはできません。 スタックに割り当てられたメモリ ブロックは、ガベージ コレクションの対象外であり、fixed ステートメントを使用してピン留めする必要はありません。

stackalloc 式 - C# リファレンス | Microsoft Docs

stackallocを利用するとスタック領域に確保できるということですね。

またメソッドを抜けると開放される(逆に抜けないと開放されないともいえる)のが普段と違うので注意です。

加えてstackallocアンマネージド型のみの配列にしか利用できません。

// 次のいずれかの型である場合、アンマネージド型

  • sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、または bool
  • すべての列挙型
  • すべてのポインター 型
  • アンマネージド型のフィールドのみが含まれるすべてのユーザー定義の構造体型で、かつ C# 7.3 以前の場合は、構築された型 (1 つ以上の型引数が含まれる型) でない

式 stackalloc T[E] では、T はアンマネージド型である必要があり、E は負でない int 値に評価される必要があります。

docs.microsoft.com

stackallocの利用

stackallocを利用しようとするとunsafeを利用することが昔は多かったですが、Spanを利用することでunsafeを利用しなくても使えます。
(Unity2021.2(C#7.2)未満の場合は、System.Memory.dllを導入する必要あり)

// int型の配列をスタック領域で確保する
int length = 3;
        
// Span(先頭のポインタと長さが格納されている)として受け取る
Span<int> numbers = stackalloc int[length];
        
// 値を変更してみる
for (var i = 0; i < length; i++)
{
    numbers[i] = i;
}

基本的にSpan<T> 型または ReadOnlySpan<T> 型を利用するのが推奨されています。

スタックに割り当てられたメモリを操作するときは、できるだけ Span<T> 型または ReadOnlySpan<T> 型を使用することをお勧めします。

stackalloc 式 - C# リファレンス | Microsoft Docs

stackalloc利用の注意点

ただしstackallocにはメモリが多く利用できない欠点があります。

スタックに割り当てたメモリが多すぎると、StackOverflowException がスローされます。
...
スタックで使用可能なメモリ容量はコードが実行される環境によって異なるため、実際の制限値は控えめに定義する必要があります。

stackalloc 式 - C# リファレンス | Microsoft Docs


ですので、あまりに要素数が多いときは普通にマネージドヒープを利用すると良いでしょう。

const int MaxStackLimit = 1024;
Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength];

stackalloc 式 - C# リファレンス | Microsoft Docs

Spanの注意点

Spanには色々な制約があるので注意してください。

Span<T>はマネージド ヒープではなく、スタックに割り当てられるref 構造体です。 ref 構造体は、ボックス化できない、Object、dynamicまたは任意のインターフェイス型の変数に割り当てられない、参照型のフィールドにできない、awaitやyieldをまたいで使用できないなど、マネージド ヒープに昇格しないようにするためのいくつかの制限があります。 さらに、Equals(Object)とGetHashCodeの2つのメソッドを呼び出すと、NotSupportedExceptionをスローします。

https://docs.microsoft.com/ja-jp/dotnet/api/system.span-1?view=net-6.0

おまけ

データをアンマネージドメモリに確保するにはMarshalクラスを利用すればいけます。
Marshal クラス (System.Runtime.InteropServices) | Microsoft Docs

// アンマネージドメモリにint配列を確保
var p = Marshal.AllocHGlobal(sizeof(int) * 8);
Span<int> unmanaged;

// Span(先頭アドレス、長さを保持)として受け取る
unsafe
{
    unmanaged = new Span<int>(p.ToPointer(), 8);
}

// GC管理下にないので、開放
Marshal.FreeHGlobal(p);