オブジェクト、文字列、配列が作成されるとき、それを格納するのに必要なメモリは ヒープ と呼ばれる中央集約的なプールから割り当てられます。アイテムが使用されなくなると、それに使用されていたメモリを何か別のもののために確保することができます。以前は、適切な関数コールを通じて明示的にヒープメモリブロックを割り当てたり解放することは、通常はプログラマ自身の責任でした。最近では Mono エンジンのようなランタイムシステムがメモリ管理を自動化しています。自動メモリ管理では、明示的に割り当て/リリースするよりコーディングの労力が大幅に削減され、かつメモリリーク (メモリを割り当て、使用しなくなってもリリースされない状況) の潜在的な可能性を著しく下げます。
関数が呼び出されると、引数の値はその特定の呼び出しのために予約されたメモリエリアにコピーされます。数バイトを占有するデータ型は速やかに簡単にコピーができます。しかし、オブジェクト、文字列、配列がそれよりも大きいことは良くあることであり、もしこのようなデータが定期的にコピーされると非常に非効率的です。幸いにこのようなことはありません。大きなアイテムのための実際のストレージのスペースはヒープから割り当てられ、小さな “ポインター” 値がその場所を覚えるために使用されます。それ以降は、ポインターをコピーすることのみがパラメーターを渡す際に必要となります。ランタイムシステムがポインターにより識別されるアイテムを見つけられるかぎり、データの 1 コピーは何回でも使用することができます。
パラメーターを渡すときに直接格納されてコピーされる型は値型と呼ばれます。これらには int、float、boolean、および Unity の struct 型 (例えば、Color や Vector3) も含まれます。ヒープに割り当てられて、ポインターを通してアクセスされる型は参照型と呼ばれます。なぜなら、変数に格納されている値はあくまでも実際のデータを “参照” しているからです。参照型の例にはオブジェクト、文字列、配列が含まれます。
メモリマネージャーはヒープの中で未使用であると認識している領域をトラッキングします。新しいメモリブロックが要求されると (例えば、オブジェクトがインスタンス化された時)、マネージャーはブロックを割り当てる未使用領域を選択し、未使用と認識されているスペースから割り当てされたメモリを取り除きます。後続の要求は、必要なブロックサイズを割り当てるために十分な空き領域がなくなるまで、同じ方法で処理されます。この時点ではヒープから割り当てられたすべてのメモリがまだ使用中であることはきわめて稀です。ヒープ上の参照アイテムは、それを指す参照変数がまだ存在するかぎりはアクセスできます。もしメモリブロックを参照するすべての参照がなくなると (すなわち、参照変数が再度割り当てされたか、参照変数がスコープ外となったローカル変数である場合) は、占有していたメモリは安全に再割り当てできます。
どのヒープブロックがもう使用されていないかを判断するために、メモリマネージャーは現在アクティブなすべての参照変数を検索して、ブロックを “活動中” としてマーキングします。検索の最後に活動中のブロックの間の空間はメモリマネージャーにより空いていると判断され、後続の割り当てで使用されます。未使用のメモリの検索や解放はガベージコレクション、略して GC と呼ばれます。
](https://www.hboehm.info/gc/)Unityは、Boehm-Demers-Weiserガベージコレクタ[、Stop-the-Worldガベージコレクタを使用しています。Unityがガベージコレクションを実行する必要があるときはいつでも、プログラムコードの実行を停止します。プログラムコードの実行を停止し、ガベージコレクタがすべての作業を終えてから通常の実行を再開します。この中断は、ガベージコレクタが処理するために必要なメモリの量や、ゲームが実行されているプラットフォームに応じて、1ミリ秒未満から数百ミリ秒に及ぶゲームの実行の遅延を引き起こす可能性があります。ゲームのようなリアルタイムアプリケーションでは、これは非常に大きな問題となります。ガベージコレクタがゲームの実行を中断すると、スムーズなアニメーションに必要な一定のフレームレートを維持することができなくなるからです。このような中断は、Profilerのフレームタイムグラフにスパイクとして表示されるため、GCスパイクとも呼ばれています。次のセクションでは、ゲーム実行中に不要なガベージコレクタによるメモリの割り当てを避けるためのコードの書き方について詳しく説明します。そうすれば、ガベージコレクタの仕事が減ります。
ガベージコレクションは自動的であり、プログラマーにとって目に見えませんが、GC プロセスは実際には裏でかなりの CPU 時間を必要とします。正しく使用されれば、自動メモリ管理によって、一般的に手動割り当てと同等か、それ以上の全体パフォーマンスを得られます。ただし、プログラマーは、コレクターを必要以上に頻繁に使用して、実行中に一時停止させるという誤りを避けることが重要です。
問題ないようにみえて GC の悪夢を発生させる悪名高いアルゴリズムもいくつかあります。繰り返しの文字列連結は昔からある事例です。
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void ConcatExample(int[] intArray) {
string line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
}
ここで鍵となるポイントは、新しい部分が文字列の所定の位置に 1 つずつ追加されるわけではないということです。実際に行われることは、ループするたびに line 変数の前回の中身が削除されます。新しい文字列全体は、もとの部分の後ろに新しい部分を加えたコンテンツに置き換えられます。文字列は i の増加分だけ長くなるため、消費されるヒープの量もまた増加し、この関数が呼び出されるたびに数百バイトの空きヒープ領域を容易に使用します。もし多くの文字列を連結する場合は、Mono ライブラリの System.Text.StringBuilder クラスを使用したほうがずっと効率的と言えます。
ただし、繰り返しの連結ですら、頻繁に呼び出されない限りそこまでの問題を引き起こしません。Unity でその頻繁となりうる要因は暗黙的にはフレーム更新です。次のような場合です。
//C# script example
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public Text scoreBoard;
public int score;
void Update() {
string scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
}
このコードで各 Update が呼び出されるたびに新しい文字列が割り当てされ、新しいガベージが常に生成されます。そのほとんどはスコアが変更されたときのみテキストを更新することで節約できます。
//C# script example
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public Text scoreBoard;
public string scoreText;
public int score;
public int oldScore;
void Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
}
もうひとつの潜在的な問題は関数が配列の値を返すときに発生します。
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements];
for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
}
この種の関数は、値で埋められた新しい配列を作成するときに、非常にエレガントで便利です。しかし、繰り返し呼び出されると、真新しいメモリが毎回割り当てられます。配列は非常に大きくなりえるため、空きヒープ領域があっという間に使い切られることがあり、結果的に頻繁にガベージコレクションが行われます。この問題を回避する方法は配列が参照型であることを活用することです。関数にパラメーターとして渡される配列はその関数の中で更新することができて、結果は関数が返した後も残ります。前述のような関数は、多くの場合に次のような関数で置き換えできます。
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
}
これは、単に既存の配列のコンテンツを新しい値で置き換えます。初期の配列の割り当てが呼び出す側のコードで行われますが (ややエレガントでありませんが)、関数が呼び出されるときに新しいガベージを生成することはなくなります。
Mono や IL2CPP スクリプティングバックエンドを使用している場合、ガベージコレクション中の CPU の急増は、実行時にガベージコレクションを無効にすることで回避できます。ガベージコレクションを無効にすると、ガベージコレクターは参照を持たないオブジェクトを収集しないため、メモリ使用量が減少することはありません。ガベージコレクションを無効にすると、実際にはメモリ使用量が増加するのみです。時間がたつにつれ増加するメモリ使用を避けるために、メモリを管理するときに注意してください。理想的には、ガベージコレクターを無効にする前にすべてのメモリを割り当て、無効になっている間に追加のメモリ割り当てを行わないようにします。
実行時にガベージコレクションを有効/無効にする方法の詳細については、GarbageCollector スクリプティング API を参照してください。
また、Incremental garbage collectionオプションを試してみることもできます。.
前述のとおり、できるかぎり割り当てを避けることがベストです。しかし、完全には排除できないことを考えると、ゲームへの影響を防ぐには主に 2 つの方法があります。
この方法がベストであるのは、長時間プレイし、スムーズなフレームレートを実現することが重要であるゲームの場合です。このようなゲームは一般的に小さなブロックを頻繁に割り当てしますが、これらのブロックは短期間しか使用されません。iOS でこのアプローチをとるときの典型的なヒープサイズは 200KB ぐらいであり、ガベージコレクションは iPhone 3G で 5ms ほどかかります。もしヒープが 1MB に増加すると、コレクションは 7ms かかります。このため定期的なフレームのインターバルでガベージコレクションを要求することにメリットがあります。厳密にはこれは一般的にコレクションを必要以上に発生させますが、素早く処理されゲームへの影響は最小で済みます。
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
しかし、このテクニックは注意して使用すべきで、プロファイラー統計をチェックして、ゲームで本当にコレクション時間が削減されていることを確認してください。
このアプローチがベストであるのは、メモリ割り当て (ひいてはコレクション) が相対的に頻繁でなく、ゲームの一時停止の際に処理できるようなゲームです。システムメモリ不足のために、OS がアプリケーションを強制終了しない範囲で、ヒープをできる限り大きくするのに役立ちます。ただし、Mono ランタイムはヒープを自動的に拡大することをできるかぎり回避します。ヒープを手動で拡大するには、起動中、プレースホルダーによってスペースを事前に割り当てします (すなわち、“無意味な” オブジェクトをインスタンス化して、メモリ管理の効果のために割り当てます)。
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (int i = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
}
十分に大きなヒープは、コレクションを伴うゲームの一時停止の合間に、完全に使い切ることはありません。もしそのような一時停止が生じる場合は、コレクションを明示的に要求します。
System.GC.Collect();
繰り返しとなりますが、このアプローチをとるときは注意すべきであり、プロファイラー統計をチェックして、思い込みでなく本当に効果が得られていることをきちんと確認すべきです。
作成および破棄されるオブジェクトの数を減らすだけで、ガベージを回避できる場合が多くあります。ゲームの中で、発射物のような特定のオブジェクトのように、いっぺんに使用することは数回しか無いにもかかわらず、何度も繰り返し使用するケースがあります。こういったケースでは、古いものを破棄して新しいもので置き換えるのではなく、オブジェクトを再利用することが可能です。
インクリメンタルガベージコレクションは、ガベージコレクションのプロセスを複数のフレームに分散させます。
インクリメンタルガベージコレクションは、Unityが使用するデフォルトのガベージコレクション方式です。これはまだ Boehm-Demers-Weiser ガベージコレクタですが、Unity はこれをインクリメンタルモードで実行します。実行するたびに完全なガベージコレクションを行うのではなく、Unityはガベージコレクションのワークロードを複数のフレームに分割します。つまり、ガベージコレクタが仕事をするために、プログラムの実行を1回の長い中断ではなく、Unityは複数回のずっと短い中断を行います。ガベージコレクションが全体的に速くなるわけではありませんが、ワークロードを複数のフレームに分散させることで、アプリケーションのスムーズさを損なうガベージコレクションの「スパイク」の問題を大幅に減らすことができます。
次のUnity Profilerのスクリーンショットは、インクリメンタルコレクションがいかにフレームレートの障害を軽減するかを示しています。これらのプロファイルトレースでは、フレームの水色の部分がスクリプト操作に使われた時間を、黄色の部分がVsync(次のフレームが始まるのを待つ)までのフレームの残り時間を、深緑の部分がガベージコレクションに使われた時間を示しています。
次のスクリーンショットは、インクリメンタルガベージコレクションを使用していないアプリケーションのUnity Profilerからのフレームキャプチャを表示しています。
インクリメンタルガベージコレクションを行わないと、60fpsのスムーズなフレームレートにスパイクが発生してしまいます。このスパイクにより、ガベージコレクションが発生したフレームは、60FPSを維持するために必要な16ミリ秒の制限を大幅に超えてしまいます(この例では、ガベージコレクションのために1フレーム以上の損失が発生しています)。
次のスクリーンショットは、インクリメンタルガベージコレクションを使用しているアプリケーションのUnity Profilerからのフレームキャプチャを表示しています。
インクリメンタルガベージコレクションを有効にした場合(上図)、ガベージコレクション操作が複数のフレームに分割され、各フレームの小さなタイムスライス(黄色のVsyncトレースのすぐ上にある濃い緑色のフリンジ)のみが使用されるため、同じプロジェクトで60fpsの一貫したフレームレートが維持されています。
次のスクリーンショットは、同じプロジェクトのUnity Profilerのフレームキャプチャを示しています。このプロジェクトでは、インクリメンタルガベージコレクションを有効にして実行していますが、今回はフレームあたりのスクリプト操作が少なくなっています。
この場合も、ガベージコレクションの操作は複数のフレームに分割されます。今回の違いは、ガベージコレクションが各フレームでより多くの時間を使い、終了までに必要な総フレーム数が少ないことです。これは、アプリケーションがVsync またはApplication.targetFrameRate を使用している場合、Unity がガベージコレクションに割り当てる時間を、残りの利用可能なフレーム時間に基づいて調整するためです。このようにして、Unityは、他の方法では待ち時間が発生する時間にガベージコレクションを実行することができるため、パフォーマンスへの影響を最小限に抑えてガベージコレクションを実行することができます。
WebGL以外のプラットフォームでは、インクリメンタルガベージコレクションに対応しています。
さらに、 VSync Count を Don’t Sync 以外に設定したり(プロジェクトのQuality settings またはApplication.VSync プロパティで)、Application.targetFrameRate プロパティを有効にした場合、Unityは自動的に、特定のフレームの終わりに残っているアイドルタイムを増分のガベージコレクションに使用します。
インクリメンタルガベージコレクションの動作をより正確にコントロールするには、Scripting.GarbageCollector クラスを使用することができます。例えば、VSyncやターゲットフレームレートを使用したくない場合、フレーム終了までの時間を自分で計算し、その時間をガベージコレクタに提供して使用することができます。
ほとんどの場合、インクリメンタルガベージコレクションは、ガベージコレクションスパイクの問題を軽減することができます。しかし、いくつかのケースでは、インクリメンタルガベージコレクションは実際には有益ではないかもしれません。
インクリメンタルガベージコレクションが作業を中断するとき。これは、管理されているすべてのオブジェクトをスキャンして、どのオブジェクトがまだ使用されていて、どのオブジェクトがクリーンアップされるかを判断するマーキングフェーズを分割するものです。マーキングフェーズの分割は、作業のスライス間でオブジェクト間の参照がほとんど変化しない場合に有効です。オブジェクトの参照が変更された場合、それらのオブジェクトは次のイテレーションで再びスキャンされなければなりません。このように、あまりにも多くの変更があると、インクリメンタルガベージコレクタに負担がかかり、マーキングパスが終了しない状況が発生します。この場合、ガベージコレクションは、完全な非インクリメンタルコレクションを行うようになります。
また、インクリメンタルガベージコレクションを使用する場合、Unityは、参照が変更されたときにガベージコレクションに通知するための追加コード(ライトバリアと呼ばれる)を生成する必要があります(ガベージコレクションは、オブジェクトを再スキャンする必要があるかどうかを知ることができます)。これにより、参照を変更する際にオーバーヘッドが発生し、一部のマネージドコードでは測定可能なパフォーマンスへの影響があります。
しかし、ほとんどの典型的なUnityプロジェクト(「典型的な」Unityプロジェクトというものがあればの話ですが)では、ガベージコレクションのスパイクに悩まされている場合は特に、インクリメンタルガベージコレクションの恩恵を受けることができます。
ゲームやプログラムが期待通りに動作するかどうかを確認するには、必ずProfiler を使用してください。
メモリ管理は巧妙で複雑なトピックであり、知識を獲得する労力も大きいものです。さらに詳しく学びたい場合は memorymanagement.org に素晴らしい情報が紹介されており、出版物やオンラン記事が一覧になっています。オブジェクトプールの詳細は Wikipedia ページ (英語) と Sourcemaking.com を参照してください。