本ノートは主に Unity エンジンのメモリ管理に関する知識についてです。
基礎概念
Unity のマネージドメモリシステムは、Mono または IL2CPP 仮想マシン(VM)に基づく C# スクリプト環境である。マネージドメモリシステムの利点は、メモリの解放を管理するため、コードで手動で解放を要求する必要がないことである。
マネージドメモリ(Managed Memory):.NET のガベージコレクタ(通常 GC と略称)によって管理され、主に C# オブジェクトの格納に使用される。
アンマネージドメモリ(Unmanaged Memory):Unity エンジンとプラグインが使用するメモリ。Texture2D、Mesh などのネイティブリソースの格納に使用される。
マネージドヒープ(Managed Heap):.NET ランタイム環境において、すべての C# オブジェクトを格納するメモリ領域である。Unity では、クラスのインスタンス、配列、文字列など、スクリプトで作成されるほぼすべてのオブジェクトが含まれる。ここで重要なのは、これらは GC によって自動管理されることである。コンピュータシステムと同様に、マネージドヒープを占有する際、異なるデータ型は異なるサイズを占有する。
自動メモリ管理
Unity では、一般的な C とは異なり、メモリヒープの占有に malloc 関数を使う必要も、使用しなくなったメモリブロックに free 関数を使う必要もない。Unity のスクリプトバックエンドはガベージコレクタを使用してアプリケーションのメモリを自動管理し、明示的な割り当て/解放と比較して、自動メモリ管理はより少ないコーディング作業で済み、メモリリークの可能性を減らす。
メモリフラグメンテーション
異なるデータ型は異なるサイズを持つため、大きなメモリブロック M を占有する必要がある場合、一部の占有メモリを解放した後でも、現在利用可能なメモリの総量が M より大きくても、M をメモリに収めることができないことがある。これは、解放されたメモリが「フラグメンテーション」しているためである。
大きなオブジェクトを割り当て、それを収容するのに十分な連続した空きスペースがない場合、上図のように、Unity のメモリマネージャーは2つの操作を実行する:
- まず、GC が(まだ実行されていない場合)実行され、割り当て要求を満たすのに十分なスペースを解放しようとする。
- GC 実行後も、要求されたメモリ量を収容するのに十分な連続スペースがない場合、ヒープを拡張する必要がある。ヒープ拡張の具体的な量はプラットフォームに依存する。ただし、ほとんどのプラットフォームでは、ヒープが拡張されるとき、以前の拡張量の2倍に拡張される。
しかし、ヒープ拡張はリスクのある操作であるため、Unity のガベージコレクション戦略はメモリのフラグメンテーションをより頻繁に行う傾向がある。また、頻繁な GC 操作はゲームのカクつきを引き起こす可能性があるため、メモリ割り当てを最小限に抑え、Update などの頻繁に呼ばれるメソッドで新しいオブジェクトを作成することを避けるべきである。
オブジェクトプール
オブジェクトプール(Object Pooling)は、必要に応じて作成・破棄するのではなく、事前にインスタンス化されたオブジェクトの集合を再利用して管理するデザインパターンである。Unity では頻繁な作成と破棄はパフォーマンス低下とメモリ割り当ての問題を引き起こすため、GC の呼び出し回数を減らすことでゲームのパフォーマンスを向上できる。
使用シーン:弾丸、パーティクルエフェクト、敵など、頻繁に作成・破棄されるオブジェクトに適している。高性能でスムーズな体験が求められるゲーム、例えばリアルタイムストラテジーゲーム、シューティングゲームで特に重要である。
実装手順
- オブジェクトプールのシングルトンを作成する;
- ゲーム開始時(Start または Awake で)必要に応じて一定数のオブジェクトを事前にインスタンス化する;
- プールからオブジェクトを取得するメソッドとオブジェクトを返すメソッドを提供する;
注意:
GC の呼び出しは確かに減るが、継続的に占有するメモリは増える。
アンマネージドリソース
アンマネージドリソースは Unity エンジンが直接管理するリソースで、.NET ガベージコレクタ(GC)の制御を受けない。テクスチャ、オーディオファイル、3D モデル、アニメーションなどが含まれる。Asset Bundles または Resources クラスでリソースをロードし、以下のメモリリークを防ぐため Destroy 関数でリソースを解放する必要がある。
Resources クラスの使用
Assets に Resources という名前のフォルダを作成する。その後、C# スクリプトで以下のように呼び出せる:
Plain Textvar prefab = Resources.Load<GameObject>("MyPrefab");
Resources.UnloadAsset(AssetName) でリソースを解放できる。Resources.UnloadUnusedAssets() を使用すると、現在使用されていないすべてのリソースを解放できる。
メモリリーク
使用されなくなったメモリが GC によって回収されないと、メモリリークが発生する。以下は発生しうるメモリリークの例である:
アンマネージドリソースの未解放
大量のアンマネージドリソース(Texture、Audio Clip、Mesh など)を作成したが、不要になったときに解放していない。
解決方法:Destroy メソッドで解放する。または使用後に .Dispose() を呼び出す。
静的変数とシングルトン
静的変数やシングルトンクラスがシーン内のオブジェクトを参照していると、シーンを切り替えてもこれらのオブジェクトは破棄されない。
解決方法:静的変数が不要になったら null に設定することを確実にする。またはシングルトンパターンの使用に注意する。
イベントとデリゲート
オブジェクトがイベントを購読しているが、破棄前に購読解除していないため、イベントがこれらのオブジェクトへの参照を保持し続ける。
解決方法:OnDestroy() メソッドで、すべてのイベントの購読を解除することを確実にする。
リソースの動的ロード
Resources.Load でリソースを動的にロードしたが、正しくアンロードしていない。これらのリソースが常にメモリに残る。
解決方法:Resources.UnloadUnusedAssets を適切に使用して、不要になったリソースをアンロードする。
メモリ割り当てを減らすためのコード最適化
文字列操作の最適化
string 型については、「+」演算を可能な限り減らす。これにより多くの一時的な文字列オブジェクトが作成されるためである。
解決方法:「StringBuilder」を使用して文字列を構築する。特にループ内で文字列を連結するシーンで。
ボックス化とアンボックス化の回避
Unity では、値型(int、float、bool など)と参照型(Object など)の区別がある。
値型と参照型の変換はボックス化とアンボックス化と呼ばれ、追加のメモリ割り当てを引き起こす可能性がある。不要なボックス化とアンボックス化の操作を避け、データ型の使用を明確にするべきである。
データ構造の最適化
まず、多くのデータ構造の長所と短所を明確にする必要がある。
各データ構造の長所・短所と複雑度を把握した上で、必要な操作を保証する前提下で、メモリ占有が小さく、操作速度が速いデータ型を可能な限り選択するべきである。
参考文献:
- Unity 3D メモリマネージメント、https://www.mvrlink.com/unity3d-managed-memory/
- オブジェクトプール、https://blog.csdn.net/l773575310/article/details/71601460
- Unity メモリリーク、https://wetest.qq.com/labs/150
| Data Structure | 利点 | 欠点 | 主要操作の時間複雑度 |
|---|---|---|---|
| List<T> | 動的配列、インデックスで要素に高速アクセス可能 | 末尾以外での挿入・削除が遅い | アクセス:O(1)、挿入/削除:O(n) |
| LinkedList<T> | 挿入・削除が速い | インデックスで直接アクセスできない | アクセス:O(n)、挿入/削除:O(1) |
| Dictionary<TKey, TValue> | 高速検索、キーと値のペアで格納、高速アクセス・更新に適している | メモリ使用量が高い、キーは一意である必要がある | 検索/挿入/削除:O(1) |
| HashSet<T> | 一意の値の集合、高速検索(存在判定)と挿入 | 要素は順序なし、要素は重複不可 | 検索/挿入/削除:O(1) |
| Queue<T> | First In First Out(先入れ先出し、FIFO) | ランダムアクセスが遅い | エンキュー/デキュー:O(1) |
| Stack<T> | Last In First Out(後入れ先出し、LIFO) | プッシュ/ポップ:O(1) | |
| Array | 固定サイズ、高速アクセス | サイズ固定、柔軟性に欠ける |
