Unity 特有(と思われる)メモリーリーク 不必要なヒープ拡張の原因とその対処法。
- Unity 2021.3.0f1 + Windows 10 + エディター上でのテスト。
StringBuilder
好きなら要注意。C# 慣れしていて配列の要素数を指定するようなことが無く List<T>
ばかり使っていれば(たぶん)起きない問題です。
雑感
- メモリをふんだんに確保してアプリのパフォーマンスを上げるのはトレンドでもあり問題ない。細かくは気にしなくてもよい。
- ただしGB単位を何度も確保されると。。。
- それを防ぐために
GC.Collect()
を使用しているが、これは思い処理とされていて気軽に呼ばないほうが良い。- ちなみにPCであればさして気にはならない。
- 他にも防ぐ方法はあるだろうが、C# のような高級言語は余程特殊な事を行っていない限りは提供されている手段を使用したほうが、パフォーマンス的にも無駄なバグを防ぐ意味でもメリットがある。なので私は
StringBuilder
を使い続ける。
問題を起こすソースコード
メモリーリーク(なのか?)が起きるコード。
👇 GameObject
に追加した時点で約 3GB のメモリが確保され、インスペクターから Invoke On Validate をオンオフするたびに追加で 3GB が確保され続ける。
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()
が特に効果を発揮しなかったのは、メモリはプロファイラーの表示の通り、意図をもって確保されているからだろう。


対処法 GC.Collect()
の使いどころ
結果的に GC.Collect()
のタイミングと、C# がなんでも間でも良い感じにしてくれるやろ、という考えが良くなかった。
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() を2回連続実行するように変更すると
// 累積的なメモリ確保を行う動作に戻る。
}
}
}
問題の起きるコードでも、ローカル変数が無くなった(ハズ)のタイミングで GC.Collect()
をしているが~とか、 C# コンソールアプリでは複数回実行してもメモリをあほ程消費しなかった~などなど気になることはあれど、バカでかい容量を作り捨てのローカル変数で確保するな、という事でしょうね。多分。。。
修正後の挙動はコメントで @albireo さんに指摘いただいた内容とも合う。
--
ネイティブプラグインとのデータのやり取りは先頭のポインタとバイト長で行え、って事なんでしょうか。
今回は GB 単位で確保したから気付いただけで、実は普段からちょこちょこと無駄なメモリを確保し続けているのかもしれませんね。怖い。
以上です。お疲れ様でした。