【Unity】 GC のスパイクを劇的に下げるコツ

<sup>更新履歴： 浮動小数点のエンディアン反転＆書き込みの効率化</sup><br/>&nbsp;


主にネットワーク処理とかでバイト配列を扱う人向けです。そうじゃない人も思い当ったら試してみてください。


- [その１： `BitConverter.GetBytes`](#その１-bitconvertergetbytes)
    - [効率的な浮動小数点のエンディアン反転と書き込み](#効率的な浮動小数点のエンディアン反転と書き込み)
    - [整数のエンディアン反転](#整数のエンディアン反転)
- [その２： `Memory<T>`](#その２-memoryt)
    - [すぐにデータを使うとは限らない](#すぐにデータを使うとは限らない)
    - [元になったバイト配列の扱い](#元になったバイト配列の扱い)
- [結果](#結果)
- [おわりに](#おわりに)



# その１： `BitConverter.GetBytes`

GC Alloc の元凶はバイト配列（参照型）を返すこのメソッド。

浮動小数点が `BinaryPrimitives` クラスで扱えないから `BitConverter` 使うかー、て感じだったけど、これを使うのをやめただけで GC Alloc が 20KB -> 10KB になってすべて解決した。

> **Note**
> ※メモリ消費量は大したものじゃないけど、10KB / 4B = 2,500 x フレームレート = 秒間数十万ものオブジェクトが GC 待ちになってスパイクが発生していたって事ですね


```csharp
BitConverter.GetBytes(floatValue);  // <-- 呼び出し毎に４バイトの配列を確保＆即GC待機列へ

// 👇 に変える

static byte[] FLOAT_BYTES = new byte[sizeof(float)];
...
UnsafeUtility.As<byte, float>(ref FLOAT_BYTES[0]) = value;
// As() メソッドは ref 戻り値で、そこに値を直接代入しているという分かり辛さ
// C ネイティブで書くとたしかこんな感じ？ --> *(float*)FLOAT_BYTES = value;

// ref var ... = ref ... でも良い。ちょっと分かりやすくなる。
ref var like_a_ptr = ref UnsafeUtility.As<byte, float>(ref FLOAT_BYTES[0]);
like_a_ptr = value;

// ※ 配列と聞くとついついクラスの静的メンバーとして一度だけ確保して、みたいにやってしまうが
// stackalloc でもマネージドヒープを回避できる。まだ慣れないなー。
Span<byte> tmp = stackalloc byte[sizeof(float)];
```

<sup>※ `UnsafeUtility.As()` は Unity.Burst に入ってる。`System.Runtime.CompilerServices.Unsafe` があるならそっちでも良い。中身は同じで間接呼び出しか否かの違いだけ。</sup>

メソッド（の `ref` 戻り値）に代入するか `ref var ...` する、ってのが分かり辛いけど `unsafe` コンテキストじゃなくても使えて便利。


## 効率的な浮動小数点のエンディアン反転と書き込み

`BitConverter` に引っ張られて `byte[]` を使っているが、そもそも４バイトなら何でもいい。

書き込み目的の反転なら `BinaryPrimitives` が扱える `Int32` を受け皿とするのが一番だろう。

```csharp
int tmp = 0;
UnsafeUtility.As<int, float>(ref tmp) = 12.345f;
BinaryPrimitives.WriteInt32BigEndian(stream, tmp);
```

これでクラスメンバーへのアクセスが無くなるので一千万回の試行でなんと 0.0x 秒も早くなる。
- https://dotnetfiddle.net/GkcZsf
- 参考： [C# の高速化・最適化関連](https://qiita.com/sator_imaging/items/0413c30716c6e5df5cd3)



## 整数のエンディアン反転

浮動小数点は上記の通り。整数の場合は Unity 2021 なら `BinaryPrimitives` が使えるから `Span<byte>` `ReadOnlySpan<byte>` とコレ使うだけでおっけ。

```csharp
BinaryPrimitives.ReverseEndianness(...);
BinaryPrimitives.ReadInt32BigEndian(...);
BinaryPrimitives.WriteInt32LittleEndian(...);

// バニラ Unity だと使えない。なので BitConverter を使いがちで問題が起きる
BinaryPrimitives.ReadSingleBigEndian(...)
```


# その２： `Memory<T>`

ネットワーク越しにデータを受信すると、受信したデータのバイト列が確保されるのは避けられない。

それは構わないとして、受信したバイト列を `Span<T>` `ReadOnlySpan<T>` を通して走査していくわけだけど、

```csharp
void OnReceived(ReadOnlySpan<byte> bytes)
{
    ...
    var processed = bytes.Slice(start, len).ToArray();  // <-- ココで新しい配列を確保
}
```

`ToArray()` で内容の一部のコピーを確保することになったりして、せっかくのスパンが台無しになる。


## すぐにデータを使うとは限らない

じゃあ `ToArray()` しなければ良いじゃない。という話なんだけど、

- C# 別スレッドで可能な限り前処理を済ませておく（Unity のメインスレッドを消費したくない
    - → その後間をおいて Unity メインスレッドで残りの処理を行う

としたい場合、短命が前提で制約の多い `Span<byte>` `ReadOnlySpan<byte>` だと無理なわけで。

で、表題の `Memory<T>` の登場となるわけだが、、、どのくらい雑に使っていいのか。


## 元になったバイト配列の扱い

受信したバイト配列を `Memory<byte>` にするのは良いとして、元になったデータの扱いはどうすればいいのかという話。例えば以下のような場合。

```csharp
...OnReceived(originalByteArray);  // バイト列がどこかで確保されてメソッドに渡される

void OnReceived(Memory<byte> bytes)
{
    ...
    _memSlices.Add(bytes.Slice(start, len));  // <-- クラスのフィールドに切り出した結果を追加
}


List<Memory<byte>> _memSlices;  // <-- Memory<T> だけが残る。元になった byte[] は忘れて良いの？？

...
_memSlices.Clear();  // どこかにある元の配列は消えるの？？
```

どこかにある匿名のバイト配列を参照している `Memory<byte>` だけが残る状態ですね。

大丈夫なの？ どうなんの？ っていう。

※ こんなこともあったので 👉 [Unity 特有の不必要なヒープ拡張とその対処法](https://qiita.com/sator_imaging/items/bb4120aa3f3830eab279)


# 結果

まあ大丈夫そうです。`unsafe` じゃないんだからそれはそう。


なにかしら参照を残しておいて `Memory<byte>` が無くなったらオリジナルを削除して、なんてことしないでも C# が全部やってくれます。良いですね！！

※ GC で回収されるタイミングはまちまち

```csharp
class MemoryAndByteArray : MonoBehaviour
{
    [SerializeField] bool ForceGCCollect = false;

    // おまじない
    void OnValidate()
    {
        if (ForceGCCollect)
        {
            ForceGCCollect = false;
            GC.Collect();
        }
    }

    // プロファイラーのスピード調整
    void Awake()
    {
        Application.targetFrameRate = 30;
    }

    // メモリー確保
    void Alloc()
    {
        var bytes = new byte[2000000000];
        var mem = bytes.AsMemory();

        for (int i = 0; i < 100; i++)
        {
            _memSlices.Add(mem.Slice(i * 200 + i, i * 100 + i));
        }
    }


    List<Memory<byte>> _memSlices = new();
    int _frameCount = 0;

    void Update()
    {
        _frameCount++;

        Debug.Log($"Frame {_frameCount:D3}:\t{GC.GetTotalMemory(false) * 0.000001:F1} MB");

        if (_frameCount == 30)
        {
            Alloc();

            // Memory<T>.Span を書き換えれば Memory<T> の内容が変わるか確認
            _memSlices[0].Span.Fill((byte)'_');
            _memSlices[0].Span[..10].Fill((byte)'A');
            _memSlices[0].Span[20..25].Fill((byte)'B');
            _memSlices[0].Span[50..^20].Fill((byte)'C');
            Debug.Log(string.Join("", _memSlices[0].ToArray().Select(static x => (char)x)));
        }
        else if (_frameCount is > 90)
        {
            for (int i = 0; i < 5; i++)
            {
                if (_memSlices.Count == 0)
                    break;
                _memSlices.RemoveAt(0);
            }
        }

    }
}
```

`Span<T>` → `Memory<T>` 変換を行うには、新規に配列を確保しないといけないの面倒っすね。`.Pin()` とかあるからなんとか出来るか？

`Span<T>` `ReadOnlySpan<T>` の強い制約は変なコードの防止になるから `Memory<T>` はなるべく使いたくないんだ。



# おわりに


メモリ使用量は多くないし、作られる配列の数がフレームレートに依存するから気付きにくい。でも直したら信じられないレベルでスパイクが無くなる。他社製のライブラリが使ってたら呼び出し頻度によっては怒りが有頂天。

今の時代にビッグエンディアンなんか使うなって話なのかもしれないけど。

--

以上です。お疲れ様でした。