Unity 特有の不必要なヒープ拡張とその対処法

Unity 特有（と思われる）~~メモリーリーク~~ 不必要なヒープ拡張の原因とその対処法。

- Unity 2021.3.0f1 + Windows 10 + エディター上でのテスト。


`StringBuilder` 好きなら要注意。C# 慣れしていて配列の要素数を指定するようなことが無く `List<T>` ばかり使っていれば（たぶん）起きない問題です。


- [問題を起こすソースコード](#問題を起こすソースコード)
    - [プロファイラーで Unity Editor を確認](#プロファイラーで-unity-editor-を確認)
- [対処法 `GC.Collect()` の使いどころ](#対処法-gccollect-の使いどころ)


<details open><summary>雑感</summary>

- メモリをふんだんに確保してアプリのパフォーマンスを上げるのはトレンドでもあり問題ない。細かくは気にしなくてもよい。
    - ただしGB単位を何度も確保されると。。。
    - それを防ぐために `GC.Collect()` を使用しているが、これは思い処理とされていて気軽に呼ばないほうが良い。
        - ちなみにPCであればさして気にはならない。
- 他にも防ぐ方法はあるだろうが、C# のような高級言語は余程特殊な事を行っていない限りは提供されている手段を使用したほうが、パフォーマンス的にも無駄なバグを防ぐ意味でもメリットがある。なので私は `StringBuilder` を使い続ける。
    - https://qiita.com/Kujiro/items/21d004bc74449765dbf7

</details>


# 問題を起こすソースコード

メモリーリーク（なのか？）が起きるコード。


👇 `GameObject` に追加した時点で約 3GB のメモリが確保され、インスペクターから Invoke On Validate をオンオフするたびに追加で 3GB が確保され続ける。


```csharp
using System.Text;
using System.Threading.Tasks;
using UnityEngine;


public class MemoryLeaker : MonoBehaviour
{
    public bool invokeOnValidate;  // インスペクターから OnValidate を実行するためのモノ


    private void OnValidate()
    {
        // Unity のメインスレッドから処理を逃がす対処法。
        // これを試したが解決できなかった。
        var value = Task.Run(async () => Problem()).Result;

        Debug.Log("OnValidate invoked");
        GC.Collect();  // 特に効果なし。

        // ココで処理を抜ければ何であれメモリは解放されるハズだが、
        // Unity ではむしろ逆でメモリを確保し始める。(約3GB)
        // StringBuilder に Append しているか否かは関係がない。
    }


    private bool Problem()
    {
        // 問題部分。
        // GB 単位だから気付いただけで普段からちょこちょこリークし続けていた？？
        var memory = new char[1000000000];  // 1GB
        var memory2 = new StringBuilder(1000000000, 1000000000);  // approx 2GB

        // ↓ C# Console App だと以下を実行して初めてメモリ確保が行われる。(2GB)
        // 何もしなければ何も起きない。メモリはこのメソッドを抜ける段階で解放される。
        // （されないともう変数にアクセスする手段がない。
        for (int i = 0; i < 1000000000; i++)
        {
            //memory2.Append("a");
        }

        return false;
    }
}
```


## プロファイラーで Unity Editor を確認

プロファイラーで確認すると、未使用のメモリを確保し続けている。Unity を終了すれば解放されるので Unity 的にはむしろ積極的に、安全側に倒すエラー対策として行っている処理なのかも。


`GC.Collect()` が特に効果を発揮しなかったのは、メモリはプロファイラーの表示の通り、意図をもって確保されているからだろう。


<img src="https://dl.dropbox.com/s/ry4wl3inu7yvqay/UnityMemoryLeaks_A.jpg" />

<img src="https://dl.dropbox.com/s/iysdj32q2fufujj/UnityMemoryLeaks_B.jpg" />




# 対処法 `GC.Collect()` の使いどころ


結果的に `GC.Collect()` のタイミングと、C# がなんでも間でも良い感じにしてくれるやろ、という考えが良くなかった。



```csharp
public class CacheManager : IDisposable
{
    StringBuilder m_sb;

    public void Dispose()
    {
        if (m_sb != null)
        {
            m_sb.Clear();
            m_sb.Capacity = 0;
            m_sb = null;
            GC.Collect();  // コレがないと実行のたびにメモリが消費される（ヒープが拡張される）
                           // ココで GC.Collect() すれば、約 2GB （拡張されたヒープ）は
                           // アプリ終了まで確保され続けるが、累積的なメモリ確保は抑えられる。
                           // （一度確保したヒープの空き部分を適切に消費してくれる）
                           // ローカル変数などは参照が無くなっていても GC.Collect() が効く
                           // とは限らないようなので注意したい。
                           // ※ ちなみに GC.Collect() を２回連続実行するように変更すると
                           // 累積的なメモリ確保を行う動作に戻る。
        }
    }
}
```


問題の起きるコードでも、ローカル変数が無くなった（ハズ）のタイミングで `GC.Collect()` をしているが～とか、 C＃ コンソールアプリでは複数回実行してもメモリをあほ程消費しなかった～などなど気になることはあれど、バカでかい容量を作り捨てのローカル変数で確保するな、という事でしょうね。多分。。。

> 修正後の挙動はコメントで @albireo さんに指摘いただいた内容とも合う。



<!--
```csharp
var value = Task.Run(async () => ProblemWrapper()).Result;
```
-->


<!--
上記の通り、`MonoBehaviour` を継承しているならその時点で打つ手はなし。まずもってこんなコードは書かないと思いますが気を付けましょう。
-->


<!--

C# コンソールアプリでテストしている段階では起きなかった問題。~~タスクとして別スレッドで実行することで解決できたので Unity 特有の問題だと思って間違いない。~~


👆 では無理だった。巨大なキャパシティーを持った `StringBuilder` を使わない対処法が効いてて上手くいったように見えただけだった。

Unity 内で巨大なキャパシティーを持った `char` `StringBuilder` を作ると、`sb.Clear();` `sb.Capacity = 0;` とかやっても確保されたメモリーが解放されることはない。。。？？

メソッド内で作り捨てじゃなく、`IDisposable` にして `using` して使用後にクリアするようにしてもダメだった。


コンソールアプリでは仕様通りに機能しているので、Unity 特有という点は間違ってはいないだろうが。。。



~~よって、とりあえずの対処法は Unity のメインスレッドから処理を逃がすことだ。~~

-->


--


ネイティブプラグインとのデータのやり取りは先頭のポインタとバイト長で行え、って事なんでしょうか。

今回は GB 単位で確保したから気付いただけで、実は普段からちょこちょこと無駄なメモリを確保し続けているのかもしれませんね。怖い。

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