ヒープメモリやらメモリ管理で検索が多いので記事にしてみたのですが 久しぶりのブログ更新なので後半部分をまとめたところで力尽きました。力尽きました
メインは海外サイトの翻訳なのですが どうにもわかりにくい。メモリ管理で検索するんだからわりと初級?だもの この説明では難しいことでしょう
後日改めて、 もう少しわかりやすくまとめた前半部分を追記しますのでこのページにリンクを貼らないで置いてください。
<文字列制限があるので記事を分割するとアドレスが変わる可能性があるのです>
うちのブログだと 8割がたゲームメーカーからのアクセスなのであんまり初心者むけ記事は見てもらえないような気もするんですけどね... (´・ω・`)
■ヒープメモリとは?
更新予定
■とりあえずはここらへんを読んでおいてください
■不必要なヒープ割り当ての一般的な原因
この元ブログ記事では以下の様な内容でメモリ管理について論じています
- メモリ管理とメモリリークの一般的な原因
- ユニティプロファイラによるメモリリークの検出
- C#でのオブジェクトプーリング
ここから元記事の意訳
- この最初の投稿は.NETとMonoのガベージコレクトされた世界のメモリ管理の基本について議論します。またメモリリークのいくつかの一般的な原因について説明します。
-
2番目の投稿はメモリリークを発見するためのツールについてです。Unityプロファイラは、この目的にとって素晴らしいツールですが、それはまた高価です。そこであなたが唯一の無料ツールでメモリリークを発見する方法をお見せするために、.NETの逆アセンブラと共通中間言語(CIL)について議論します。 -
3番目の投稿は、ふたたびC#のオブジェクトプーリングを議論します。ここでも焦点はUnity/ C#の開発で生じる特定のニーズです。
■ガベージコレクションの限界
最近のほとんどのオペレーティング・システムでは、スタックとヒープに動的メモリを分割します。多くのCPUアーキテクチャ(PC/Mac そしてスマートホン/タブレットが含まれます)でこのような命令セットがサポートされています。c#では値の型を区別することでこれらをサポートしています(単純な組み込み型などとして宣言されているユーザー定義型 enum または struct)と参照型(クラス、インタフェース、およびデリゲート) 値型はスタックに参照型はヒープに割当てられます。スタックは新しいスレッドの開始時に設定された固定サイズを有しています。これは通常は小さくて たとえばwindowsのデフォルトの.Netスレッドは1Mb。このメモリはスレッドの主な機能とローカル変数をロードし メイン関数から呼び出される関数(それらのローカル変数)をアンロードするために使用されます。その一部は動作を高速化するためにCPUのキャッシュにマッピングすることができます。コールが深すぎないかローカル変数は巨大すぎないかといったスタックオーバーフローを恐れる心配はありません。スタックのこの使われ方は構造化プログラミングの概念と整合することを確認してください。
オブジェクトがスタックに対して大きすぎる場合またそれらの機能がスタックに長く残ってしまった時にヒープが発生します。ヒープはそれ以外のメモリの箇所で 通常はOSとプログラムの規則の要求通りに増加します。一方スタックにある場合はメモリ管理をするのは簡単で(フリーなメモリの場所を記憶するためにはポインタが使用されます) ヒープを管理することは,ヒープが断片化した場合すぐに割当てられたオブジェクトからオブジェクトを開放させる要求をださなければならないことは明らかで,すべてのスイスチーズの穴のような場所を覚えることに気を止めなければならず 全く楽しくありません。そこで自動メモリ管理を導入します。 自動割当てのタスクは 主にあなたにとってのすべてのチーズの穴のようなメモリ断片を追跡する簡単なもので実質すべてのプログラミング言語でサポートされています。 自動割当てをする決定の多くの困難なタスクを特にあなたが心配する必要はなくオブジェクトは割当て解除の準備ができています。
この後者のタスクは、ガベージコレクション(GC)と呼ばれています。あなたのランタイム環境を伝える代わりに 実行時に一定の間隔でオブジェクトへのすべての参照を追跡し確定後オブジェクトが破壊されてメモリが解放されたときに おそらくコードからオブジェクトに到達できなくなります。ガベージコレクトは未だに学者によって活発に研究されていることが ガベージコレクトのアーキテクチャが。Netフレームわーっクから大幅に変更と改善をされている理由です。またUnityはMONOの最新バージョン(2.11 / 3.0)にデフォルト設定はありませんが、その代わりに バージョン2.6を使用しています(正確には、私のWindowsの2.6.5は、Unityの4.2.2 がインストールされています。Unty4.3についても同様です) あなたが自身でこれらを確認する方法に不明な点があるようであれば 次の投稿でそれを説明していきます。
バージョン2.6以降のMONOに導入されたGC関連は主要な改変のひとつです。新しいバージョンがGenerational GC(世代別GC)を使用しているのに対し バージョン2.6ではまだあまり洗練されていない ベームガベージコレクタを使用しています。最近の世代別GCはゲームなどのリアルタイムアプリケーションのために(制限内ですが)良好に動作します。ベームスタイルのGCは一方で比較的まれな間隔(通常は一回あたりのフレームのゲームに比べてはるかに少ない頻度)でヒープ上のガベージごみを徹底的な検索を実行して動作します。これによって一定の間隔でフレームレートの低下が引き起こされる圧倒的な傾向があり プレーヤーは迷惑を蒙(こうむ)ります。UnityのドキュメントではFPSが低下する状態の場合にはSystem.GC.Collect()を呼ぶことを推奨しています(例えば、新しいレベルのロードやメニューを表示する時) しかし多くのゲームの種類においてこのような機会はまれにしか発生せず あなたがGCのをしたい時にガベージコレクトを拒否するかもしれません。あなたの唯一の選択肢は勇敢にメモリを自分で管理することで、それについて残りの2つの記事で述べています。
- 世代別ガベージコレクション - Wikipedia
- Boehm garbage collector - Wikipedia, the free encyclopedia
- Boehm GC - HoneyComb
■メモリマネージャを適切なものに
ユニティ/ .NETの世界では「メモリを自分で管理する」ことを意味するかについて明確にしてみましょう。メモリ割当てがされる方法に影響を与えるためのあなたの力は(幸いなことに)非常に限られています。あなたは クラス(常にヒープ上に割り当てられた)または 構造体(クラス内に含まれていない限りスタック上に割り当てられます)のカスタムデータ構造を選択し採用することができます。あなたがより多くの魔法の力が必要な場合は、C#のUnsafe コードのキーワードを使用する必要があります。しかしUnsafe コードは、UnityのWeb Player及びおそらく他のいくつかのターゲットプラットフォームで実行されないことを意味している検証できないコードです。このことと その他の理由により安全ではありませんので使用しないでください。
※Unite EUROPE2015のKEYNOTEを観る限りではUnsafeコードは普通に使用されているようですが?
そのためスタックの上記の制限およびC#の配列がためだけの糖衣構文であるためのSystem.Array(これらはクラスです)自動ヒープ割当てを避けることはできません。不要なヒープの割り当てを何によって避けるべきかをこの記事の次の(そして最後の)セクションで知ってください。
メモリ割当て解除されるまではあなたの及ぶ力は限定的です。実際ヒープされたオブジェクトの割り当てを解除することができる唯一の方法はGC(ガベージコレクト)であり その働きはあなたから遮蔽されています。ヒープ上のオブジェクトのいずれかへの最後の参照がスコープ外になったときにGCがその前にそれらに触れることができないため、何らかの影響を与えることはできます。定期的なガベージコレクション(あなたが抑制することができない)が解放するものがないときには非常に高速になる傾向があるため この制限された力は、たいへん実用的な関連性を持っていることが判明しました。この事実は様々なアプローチの基礎となるオブジェクトプーリングについて3番目の投稿で議論します。
- http://www.gamasutra.com/blogs/WendelinReich/20131127/203843/C_Memory_Management_for_Unity_Developers_part_3_of_3.php
- unsafe コードとポインター (C# プログラミング ガイド)
- c# - Safe vs Unsafe code - Stack Overflow
■foreachループを避けるべきでしょうか?
foreachループを使用する代わりにforまたはwhileループを使用するべき理由は、foreachは実際には単なるシンタックスシュガー(糖衣構文:読み書きのしやすさのために導入される構文)で、以下のようなコンパイラの前処理コードだからです。
foreach (SomeType s in someList) s.DoSomething(); |
...次のように:
using (SomeType.Enumerator enumerator = this.someList.GetEnumerator()) { while (enumerator.MoveNext()) { SomeType s = (SomeType)enumerator.Current; s.DoSomething(); } } |
foreach はenumeratorオブジェクトを生成します。 System.Collections.IEnumeratorインターフェースのインスタンス シーンの裏側で このオブジェクトはスタックとヒープのどちらに生成されるのでしょうか?両方が実際に可能であるため これは大変よい質問です。もっとも重要なことはSystem.Collections.Generic の namespace (List
foreachのループを回避する必要がありますか?Unityでコンパイル可能なC#のコードでそれらを使用しないでください。foreachを標準のgeneric collections (List
※2
■あなたはclosuresやLINQを避けるべきでしょうか?
C#では匿名メソッドやラムダ式(かなり互いに同一)が提供されていてdelegateキーワードと=>演算子 を用いてそれらを作成することが出来ます。これらはしばしば便利なツールであり List<T>.Sort()) や LINQ といったライブラリ関数を使用するときたいへん重宝します。 匿名メソッドとラムダ式は、メモリリークを起こしているでしょうか? それは依存しています C#コンパイラには、実際にそれらを処理する2つの非常に異なった方法があります。違いを理解するために次のコードを考慮してください。
int result = 0;
void Update()
{
for (int i = 0; i < 100; i++)
{
System.Func<int, int> myFunc = (p) => p * p;
result += myFunc(i);
}
}
|
このようにスニペットmyFuncというdelegateを1フレームあたり100回生成する計算を実行します。しかしながら Monoはヒープ割当てを最初にUpdate()関数(※筆者の環境で52Byte)が呼ばれた時だけ実行します。サブシーケンスでヒープ割当ては実行されません。どうなっているのでしょう?コードリフレクタ(part2を参照)を使用すると C#コンパイラはmyFunc型の静的フィールドでSystem.Func
今度は、デリゲートの定義にマイナーチェンジを作ってみましょう:
|
closuresは関数型プログラミングの柱です。closuresはデータに関数を結びつけます、より正確には関数外で定義される非ローカル変数に結びつけます。 このmyFuncの例の場合'p'はローカル変数ですが'i'はUpdate()関数に属する非ローカル変数です。 C#コンパイラはmyFuncをアクセスと非ローカル変数を更新することができるように変換する必要があります。 リファレンス環境にあわせて新規にクラスを宣言することでmyFuncの生成を実現しています。 このクラスのオブジェクトは、for-loopを通過するたびに割当てられるため(※筆者の環境ではフレームごとに2.6kb) 突然巨大なメモリリークに襲われます。
もちろん closuresや他の言語機能がC#3.0で導入された主な理由はLINQです。closuresがメモリーリークに繋がることができればゲームでLINQを使用しても大丈夫でしょうか?LINQの一部はiOSなどの実行時コンパイラをサポートしないOS上で明らかに動作しません。メモリ管理の側面からLINQはとにかく良くありません。
次の信じられないような基本的な表現:
int[] array = { 1, 2, 3, 6, 7, 8 }; void Update() { IEnumerable<int> elements = from element in array orderby element descending where element > 2 select element; ... } |
毎フレーム68バイトを割当て( Enumerable.OrderByDescending() が28バイト、 Enumerable.Where()で40バイト)!
ここでの犯人はclosuresではありません 拡張メソッド IEnumerable:LINQは最終的な結果を出力するために 中間の配列を作成する必要があり そのあとそれらをリサイクルするシステム領域を持ちません。(※私はLINQの専門家ではないので リアルタイムの実行環境で安全にそれらのコンポーネントを使用する方法はわかりません)
■コルーチン
あなたがStartCoroutine()経由でコルーチンを起動した場合は、暗黙的にunityのインスタンスコルーチンのクラス(私のシステムで21バイト)とEnumerator(16バイト)の両方を割り当てます。yield's や resumesコルーチンは何も割当てをしません そこでメモリリークを回避するためにはゲームが実行されている間StartCoroutine()の呼び出しを制限してください。
void Update()
{
string string1 = "Two";
string string2 = "One" + string1 + "Three";
}
|
■ストリングス
C#とunityのメモリに関しては文字列に言及しない訳にはいきません。文字列は変わっていて、ヒープ割当てでありながらイミュータブルです。2つの文字列を連結するとき(これらの変数や文字列定数で)ランタイムは 結果を含む少なくとも1つの新しい文字列オブジェクトを割り当てることがあります。String.Concat()これはFastAllocateString()と呼ばれる外部メソッドにより効率的に行われます。(ただし上記の例ではシステム上の40バイト)。 変更または実行時に文字列を連結する必要がある場合にはSystem.Text.StringBuilderを使用します。
※オブジェクト指向プログラミングにおいて、イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクト のこと
■ボクシング
時々データがスタックとヒープを移動する必要があります。例えば 次のように文字列のフォーマットを設定する場合です:
|
次のシグネチャを持つメソッドを呼び出しています。
public static string Format( string format, params Object[] args ) |
言い換えると、整数「5」および浮動小数点数「5.0f」はSystem.ObjectがFormat()を呼び出した際にキャストする必要があります。 しかし、オブジェクトが参照型なのに対して他の2つは値型です。 C#はこのようにヒープ上の割り当てられたメモリに値をコピーすることがあり、Format()はint型とfloat型のオブジェクト参照を新しく作成します。 このプロセスがボクシング、 反対の動作をアンボクシングと言います。 このstring.Format()のビヘイビアは問題となるかもしれません すなわちヒープメモリからとにかくそれを取り除く必要があります(新しいstring のために) しかしボクシングは期待される場所に出現するわけではありません。悪名高い例として 等価演算子"=="を実装するとき (例えば、複雑な数を表す構造体)
※このような場合のボクシングを避ける方法についての記事をリンク先で読むことが出来ます。
■ライブラリメソッド
public static class ListExtensions { public static void Reverse_NoHeapAlloc<T>(this List<T> list) { int count = list.Count; for (int i = 0; i < count / 2; i++) { T tmp = list[i]; list[i] = list[count - i - 1]; list[count - i - 1] = tmp; } } } |
ここから、これまでの考察をまとめた別記事の意訳です
■Unity、C#と.NET /モノラルでメモリ使用量を削減
iOSの上のUnityはMonoのヒープマネージャの初期のバージョンを使用しています。このマネージャは、パッキングを行いませんので、ヒープメモリが断片化された場合は新しいメモリを確保してしまいます。私はUnity開発者がこの問題を回避するために新しいヒープマネージャで作業している印象なのですが、今のところメモリリークを持つゲームは増え続けるメモリを消費してしまいます。C#は,すばやく読みやすさを犠牲にすることなく強力なコードを記述することができる楽しい言語です。しかしこの方法の欠点は自然なC#のコードを書くことがガベージコレクションの多くを生成してしまうことです。これを回避する唯一の方法はヒープ割当てを排除または低減することです。私は機能性を低下させることなく これを行うための便利な方法をリストにまとめました。最終的な効果として あなたのC#コードは,はるかにC ++のように見えC#の力の一部を失ってしまうものの それにより生きてきます。副次効果としてヒープアロケーションは本質的にスタックアロケーションよりもよりCPU集約型なので、おそらく同様にいくつかのフレーム時間を節約できます。あなたの努力目標のためにユニティプロファイラは多くのメモリ割当てを作る作業を助けてくれます。情報は多くはないですが。プロファイラを開きゲームを実行し、CPUプロファイラを選択して、問題箇所をソートするためのGCアロケータカラムをタップします。はじめにそれらの機能にこれらのガイドラインを適用してください。
■foreach()を使用しないでください。
GetEnumerator()を呼び出しlist型にヒープ上のenumeratorを割り当てるのをすぐに止めてください。for(;;)構文に,より詳細なC ++スタイルを使用することが必要になります: あなたが配列上にforeach-ingしなければ,それは何もメモリを割当てません。私はそれがfor(...){}のための構文糖衣(読み書きのしやすさのために導入される構文)になった特殊なケースだと推察します。
Ian Horswill氏このポイントを指摘していただき有り難うございます。
■strings文字列を避けてください。
文字列は.NETでは不変でヒープメモリ領域に割り当てられています。UIのためC言語の様にその場で それらを操作することはできません StringBuilders で、できるだけ文字列への変換を遅らせてメモリ効率の良い方法で文字列を構築します。あなたはリテラルがメモリ内の同じインスタンスを指す必要があるためキーとしてそれらを使用することができますが、あまりにも多くの操作はしないでください。
■構造体を使用してください。
MonoのStruct型はスタック上に割り当てられています、ユーティリティクラスを持っていれば構造体作成にスコープを残すことはありません。任意のコピーのコストを回避するためにrefとパラメータの前に付ける必要があります。構造体は値で渡されることを覚えておいてください。
■スコープ結合を固定サイズ配列の構造体に交換してください。
固定サイズの配列を持っている場合スコープを残さずメンバ配列を再利用できます。またはそれをミラーフィールドを持つ構造体を作成することができます メンバー配列に置き換えることのいずれかを検討してください。 私は4つのフィールドを持っているスプラインクラスのControlList構造体を呼び出すときはいつもVector3 [4]に置き換えます。そしてインデックス基準のアクセスのためにthis[]プロパティを追加しました。それは頻繁に呼び出される関数だったので大量のメモリ割当てを回避できました。
■リストを受け渡す際refキーワードを使用した参照渡しが好ましいです。
あなたはヒープメモリに渡すリストを必要とするのが正解で、これで何も変わらないように聞こえますか? ですがこれにより必要に応じて最適化を可能にします。
■頻繁な呼び出しをする関数をメンバ変数としてをローカルストレージにスコープすることを検討してください。
毎回大きなリストを呼びださなければならない時,リストにメンバ変数が作成されストレージはフレーム間持続します。 次のフレームにはメモリ領域が不足するのでC#のリストでバッファ領域から削除するためClear()を呼びださなければいけません。それによりコードが酷く読みにくくなるので、わかりやすいコメントが必要になりますが 作業は大幅に軽減できます。
■IEnumerable拡張メソッドを避けてください。
ほとんどの便利なLINQのIEnumerable拡張メソッドは、新しい割り当てを作成することは言うまでもありません。私は.Any()が IList<>で呼びだされた時 Count> 0で Virtual関数がメモリ割当てを引き起こす First() とLast()のようなIlist()関数のメソッドも同様だと考えたのですが予想に反していました。この結果とforeach()の制限から あなたのインタフェースにはIEnumerable <>抽象化を避けて代わりにIList <>を使用するべきでしょう。
■関数ポインタ使用を最小限に 。
クラスメソッドをデリゲートやFunc<>に代入するとボックス化の原因となりメモリ割当てを引き起こします。私にはボクシングのないメソッドへのリンクを格納するための任意の方法はわかりません。デカップリングに大きな恩恵があるため割当てられたメモリは残り続けますが関数ポインタのほとんどは残してきましたが,いくつかは削除しました。
※デカップリング(Decoupling) プログラミングとデザインで、一般的に 再利用され可能な限り少数の依存関係を使用してコードを作成する行為のこと
■クローン化されたマテリアルに注意してください。
任意のレンダラのマテリアルプロパティを取得する場合、それに何も設定しない場合でもマテリアルを複製します。このマテリアルはGC'dされていません レベルの変更(シーンロード)またはResources.UnloadUnusedAssetsを呼び出したときのいずれかの場合のみクリーンアップされます。マテリアルを調整する必要はありませんが知っていればmyRenderer.sharedMaterialを使用してください。
※MaterialPropertyBlock を使用することで改善できる場合があります
■ディクショナリキーとして生の(未加工)構造体を使用しないでください 。
Dictionary
ライアン・ウィリアムズ 氏https://twitter.com/quxmore のおかげで,これを見つけられました。
■Enum.GetValues()またはmyEnumValue.ToString()を使用し過ぎないようにしてください。
Enum.GetValues(typeof(MyEnum))は呼びだされるたびに配列を割り当てます。Enum.GetNames()も同様です。他の場所同様にUIを扱う際enum変数に.ToStringを加えて使用すべきです。enum値をループなどcachedEnumNames [(int)myEnumValue]に使用するように簡単にこれら両方の配列をキャッシュすることができます。enum型の値を手動で設定している場合 例えばフラグですが、これは動作しません。
■C# Coroutine WaitForSeconds Garbage Collection tip - Unity Community
C# Coroutine WaitForSeconds Garbage Collection tip - Unity Community
追記:メモリ最適化の実装例としてWaitForSecondsコルーチンを最適化してガベージコレクトを抑える記事を簡単にまとめておきます。
詳細はリンク先の記事で確認してください
A: <Before>"yield return new WaitForSeconds()" は呼び出されるごとにガベージメモリを21バイト消費します
("yield return null"なら9バイトのメモリ消費)。
これにより一回のIEnumeratorメソッドの呼び出しを9バイトに減らすことが出来ました。
B:Yieldメソッドを場合分けして辞書登録し Yieldersクラスとして呼び出します。WaitForSeconds()メソッドは直値を指定すると A:の例では”0.1f、0.5f"のフロート型の数値ですが、呼び出されるたびにヒープメモリには新規に数値がキャッシュされ これが9バイトのメモリ消費となってしまいます。
そこでUnityのシステムが管理する値 WaitForFixedUpdate()に代入される数値をstatic型で置き換えることで、値がヒープメモリに複製されないようにしています。
A:も ガベージコレクト対策としては悪いアイデアではないのでケースに応じて使い分けることが良いかもしれません。
IEnumeratorメソッドはゲームオブジェクトのスクリプトに含まれることが多いためアタッチされたインスタンスが数百単位で複製される場合もあり 多くのヒープメモリを消費してしまいます。そのためガベージコレクト回避の対策として"Yield〜WaitForSeconds()" まわりを最適化することは有効だと考えられそうです。
A:
|
IEnumerator myAwesomeCoroutine() { while (true) { doAwesomeStuff(); yield return new WaitForSeconds(waitTime); } } |
|
WaitForSeconds shortWait = new WaitForSeconds(0.1f); WaitForSeconds longWait = new WaitForSeconds(5.0f);
IEnumerator myEvenAwesomerCoroutine() { while (true) { if (iNeedToDoStuffFast) { doAwesomeStuffReallyFast(); yield return shortWait; } else{ dontDoMuch(); yield return longWait; } } }
|
B:
|
using UnityEngine; using System.Collections; using System.Collections.Generic;
public static class Yielders {
static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100);
static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame(); public static WaitForEndOfFrame EndOfFrame { get{ return _endOfFrame;} }
static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate(); public static WaitForFixedUpdate FixedUpdate{ get{ return _fixedUpdate; } }
public static WaitForSeconds Get(float seconds){ if(!_timeInterval.ContainsKey(seconds)) _timeInterval.Add(seconds, new WaitForSeconds(seconds)); return _timeInterval[seconds]; }
} |
<呼び出し側>
必要な時間間隔にするにはYieldメソッドを複数回記述する。
| yield return Yielders.Get(this.updateinterval);
|
【追記】
『テラシュールブログ』さんでUNITYのパフォーマンス最適化について、まとめられているそうですので興味のある方は参照してください。
もう一か所、自分の説明不足のところをフォローしていただいたようなので、こちらの記事も参照してみてください。
■ 更新ついでに近況 … 仕事が一段楽したので近々ブログ復帰予定です. (10/29)
■リファレンス

























