UnityのGC AllocateとWaitForSeconds

[PONOS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ponos)の1日目の記事です。

# はじめに
ご存知の通りUnityにおけるメモリ管理は基本的にGCにおまかせになっています。
詳しい説明は省略しますが、一般的にこうした環境下でのメモリの領域は、大きくわけてヒープ領域とスタック領域という二つに分類されます。
ローカル変数として宣言された値型やstructはスタック領域にメモリが確保され、スコープを抜けたタイミングで解放されるわけですが、ヒープ領域に確保されたメモリは特定のタイミングでGCによって解放されます（世代別GCなどの話は省略）。
このヒープ解放はそれなりに負荷がかり、時にはスパイクとなってしまうことがあり注意が必要です。

Unityのコルーチンで用いられる`WaitForSeconds`はClassインスタンスを生成するため、ヒープ領域にメモリが割り当てられます。
つまり、ループ毎に `new WaitForSeconds(xxx)` を行ってしまうと、理論上はその度にヒープ領域にゴミのようなメモリ割り当てを発生させてしまうことになります。

今回はProfilerを使用して、実際に`new WaitForSeconds(xxx)` がどの程度メモリ割り当てを発生させているかを確認してみたいと思います。

# 検証内容
検証する環境は以下の通りです。

- Unity: *2020.3.23f1*
- フレームレート: *60FPS* 
- Scene上にはデフォルトのカメラとライト以外、テスト用のGameObjectのみとする
- 極力他の影響を避けるため、デバッグログの出力なども含めた余計なコードは一切かかない
- 動作はUnityエディタ実行時で確認する

そして以下の動作を確認します。

1. 何もしない状態で毎フレームどの程度のメモリー割り当てが発生しているかを確認します。
2. `new WaitForSeconds(xxx)`を行うコルーチンを100個生成し、同様の確認を行います。
3. `new WaitForEndOfFrame()`を行うコルーチンを100個生成し、同様の確認を行います。
4. `WaitForSeconds`を再利用する形に変更し、同様の確認を行います。

# 検証する
## 1. プレーンな状態
#### テスタースクリプトを作る
のちのち`WaitForSeconds`を使用するテスタースクリプトを作ります。
ここではプレーンな状態を想定しているので実際にはそれを使用しませんが、FPSだけ固定しておきます。

```C#
public class Tester : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;
    }
}
```

このスクリプトをヒエラルキーのGameObjectにアタッチしておきます。

#### 結果を見る
`Window > Analysis` を開き、この状態でエディタ実行してしばらく様子をみます。
`Memory`の項目の`GC Allocated In Frame`という項目を見ると、１フレームあたりに割り当てられた個数とデータ量が確認できます。

![スクリーンショット 2021-11-29 19.50.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/fad451cc-297c-8e65-384b-65096711a4d0.png)
![スクリーンショット 2021-11-29 19.50.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/5e779ecd-2199-89d7-edc7-6a60303c1b25.png)

この結果から、プレーンな状態では`0/0B`であることがわかりました。

## 2. `new WaitForSeconds(xxx)`を行うコルーチンを100個生成
#### スクリプトを編集する

0.01f秒のwaitを返すコルーチンを100個生成するようにしてみました。

```C#
public class Tester : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;
        for (var i = 0; i < 100; i ++) {
            StartCoroutine(Hoge());
        }
    }

    private IEnumerator Hoge()
    {
        while (true) {
            yield return new WaitForSeconds(0.01f);
        }
    }
}
```

#### 結果を見る

![スクリーンショット 2021-11-29 20.00.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/e7db8111-9db9-45d3-b152-c670e47bfd6f.png)
![スクリーンショット 2021-11-29 20.00.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/f786e0ec-53cc-7f38-462e-4023e851a4f8.png)

`Memory`の項目の`GC Allocated In Frame`という項目を見ると、毎フレーム**100個/2.0KB**のメモリ割り当てが発生していることが確認できます。
60FPSなので１秒間で120KB, １分間に約7MBの割り当てを行なっている計算です。
また、`WaitForSeconds`一つあたり、20Bのメモリが割り当てられるようです。

## 3. `new WaitForEndOfFrame()`を行うコルーチンを100個生成
#### スクリプトを編集する
次は`WaitForEndOfFrame`の場合をみてみます。
コルーチンを書き換えます。

```C#
    private IEnumerator Hoge()
    {
        while (true) {
            yield return new WaitForEndOfFrame();
        }
    }
```

#### 結果を見る
![スクリーンショット 2021-11-29 20.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/95ca1c68-72b2-909b-e300-6db1055a2f03.png)
![スクリーンショット 2021-11-29 20.04.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/2f4493cc-919a-bc71-6da8-2fce38f98cd9.png)

`Memory`の項目の`GC Allocated In Frame`という項目を見ると、毎フレーム**100個/1.6KB**のメモリ割り当てが発生していることが確認できます。
60FPSなので１秒間で96KB, １分間に約5.6MBの割り当てを行なっている計算です。
また、`WaitForSeconds`一つあたり、16Bのメモリが割り当てられるようです。

## 4. `WaitForSeconds`を再利用する
ここまでのことでわかる通り、このループのコードでは常にメモリ割り当てが発生してしまっています。
これを発生させないために再利用するコードに変えてみます。

#### スクリプトを編集する
コルーチンの処理を書き換えます。

```C#
    private IEnumerator Hoge()
    {
        var wait = new WaitForSeconds(0.01f);
        while (true) {
            yield return wait;
        }
    }
```

#### 結果を見る
![スクリーンショット 2021-11-29 20.09.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/b0c9b38c-5ae6-35d5-8570-c59adbc48272.png)
![スクリーンショット 2021-11-29 20.09.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911864/6cffe535-f979-5637-9e56-4f1f5bba9689.png)

`Memory`の項目の`GC Allocated In Frame`という項目を見ると、**0/0B**になっており、メモリ割り当てが発生していないことが確認できます。

# まとめ
このように、よく使う`WaitForSeconds`も、ヒープ領域が割り当てられることが確認できました。
１インスタンスあたりの割り当てられるメモリは決して多くありませんが、同様のオブジェクトが多数あったり、１秒間あたりの実行回数が多い場合は無視できないデータ量になることもあり、それによってGCのためのスパイクを発生させる要因の一つとなりえます。
ここではインスタンスの再利用という簡単なコードでそれを回避しました。

`WaitForSeconds`に限らず、小さいデータであったとしてもヒープ領域へのアロケーションが必要なコードをループに記述する場合、極力その回数を減らせられるように、注意が必要です。

明日は[@e73ryo](https://qiita.com/e73ryo)さんです！