Version: 2019.4
言語: 日本語
イベント関数の実行順序
プラットフォーム依存コンパイル

自動メモリ管理

オブジェクト、文字列、配列が作成されるとき、それを格納するのに必要なメモリは ヒープ と呼ばれる中央集約的なプールから割り当てられます。アイテムが使用されなくなると、それに使用されていたメモリを何か別のもののために確保することができます。以前は、適切な関数コールを通じて明示的にヒープメモリブロックを割り当てたり解放することは、通常はプログラマ自身の責任でした。最近では Mono エンジンのようなランタイムシステムがメモリ管理を自動化しています。自動メモリ管理では、明示的に割り当て/リリースするよりコーディングの労力が大幅に削減され、かつメモリリーク (メモリを割り当て、使用しなくなってもリリースされない状況) の潜在的な可能性を著しく下げます。

値型と参照型

関数が呼び出されると、引数の値はその特定の呼び出しのために予約されたメモリエリアにコピーされます。数バイトを占有するデータ型は速やかに簡単にコピーができます。しかし、オブジェクト、文字列、配列がそれよりも大きいことは良くあることであり、もしこのようなデータが定期的にコピーされると非常に非効率的です。幸いにこのようなことはありません。大きなアイテムのための実際のストレージのスペースはヒープから割り当てられ、小さな “ポインター” 値がその場所を覚えるために使用されます。それ以降は、ポインターをコピーすることのみがパラメーターを渡す際に必要となります。ランタイムシステムがポインターにより識別されるアイテムを見つけられるかぎり、データの 1 コピーは何回でも使用することができます。

パラメーターを渡すときに直接格納されてコピーされる型は値型と呼ばれます。これらには int、float、boolean、および Unity の struct 型 (例えば、ColorVector3) も含まれます。ヒープに割り当てられて、ポインターを通してアクセスされる型は参照型と呼ばれます。なぜなら、変数に格納されている値はあくまでも実際のデータを “参照” しているからです。参照型の例にはオブジェクト、文字列、配列が含まれます。

割り当てとガベージコレクション

メモリマネージャーはヒープの中で未使用であると認識している領域をトラッキングします。新しいメモリブロックが要求されると (例えば、オブジェクトがインスタンス化された時)、マネージャーはブロックを割り当てる未使用領域を選択し、未使用と認識されているスペースから割り当てされたメモリを取り除きます。後続の要求は、必要なブロックサイズを割り当てるために十分な空き領域がなくなるまで、同じ方法で処理されます。この時点ではヒープから割り当てられたすべてのメモリがまだ使用中であることはきわめて稀です。ヒープ上の参照アイテムは、それを指す参照変数がまだ存在するかぎりはアクセスできます。もしメモリブロックを参照するすべての参照がなくなると (すなわち、参照変数が再度割り当てされたか、参照変数がスコープ外となったローカル変数である場合) は、占有していたメモリは安全に再割り当てできます。

どのヒープブロックがもう使用されていないかを判断するために、メモリマネージャーは現在アクティブなすべての参照変数を検索して、ブロックを “活動中” としてマーキングします。検索の最後に活動中のブロックの間の空間はメモリマネージャーにより空いていると判断され、後続の割り当てで使用されます。未使用のメモリの検索や解放はガベージコレクション、略して 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();

繰り返しとなりますが、このアプローチをとるときは注意すべきであり、プロファイラー統計をチェックして、思い込みでなく本当に効果が得られていることをきちんと確認すべきです。

再利用可能なオブジェクトプール

作成および破棄されるオブジェクトの数を減らすだけで、ガベージを回避できる場合が多くあります。ゲームの中で、発射物のような特定のオブジェクトのように、いっぺんに使用することは数回しか無いにもかかわらず、何度も繰り返し使用するケースがあります。こういったケースでは、古いものを破棄して新しいもので置き換えるのではなく、オブジェクトを再利用することが可能です。

インクリメンタルガーベッジコレクション

Incremental Garbage Collection spreads out the work performed to perform garbage collection over multiple frames.

With Incremental garbage collection, Unity still uses the Boehm–Demers–Weiser garbage collector, but runs it in an incremental mode. Instead of doing a full garbage collection each time it runs, Unity splits up the garbage collection workload over multiple frames. So instead of having a single, long interruption of your program’s execution to allow the garbage collector to do its work, you have multiple, much shorter interruptions. While this does not make garbage collection faster overall, it can significantly reduce the problem of garbage collection “spikes” breaking the smoothness of your game by distributing the workload over multiple frames.

The following screenshots from the Unity Profiler, without and with incremental garbage collection enabled, illustrate how incremental collection reduces frame rate hiccups. In these profile traces, the light blue parts of the frame show how much time is used by script operations, the yellow parts show the time remaining in the frame until Vsync (waiting for the next frame to begin), and the dark green parts show the time spent for garbage collection.

Nonincremental garbage collection profile
Nonincremental garbage collection profile

Without incremental GC (above), you can see a spike interrupting the otherwise smooth 60fps frame rate. This spike pushes the frame in which garbage collection occurs well over the 16 millisecond limit required to maintain 60FPS. (In fact, this example drops more than one frame because of garbage collection.)

Incremental garbage collection profile
Incremental garbage collection profile

インクリメンタルガベージコレクションを有効にした場合(上図)、ガベージコレクション操作が複数のフレームに分割され、各フレームの小さなタイムスライス(黄色のVsyncトレースのすぐ上にある濃い緑色のフリンジ)のみが使用されるため、同じプロジェクトで60fpsの一貫したフレームレートが維持されています。

Incremental garbage collection using left over time in frame
Incremental garbage collection using left over time in frame

This screenshot shows the same project, also running with incremental garbage collection enabled, but this time with fewer scripting operations per frame. Again, the garbage collection operation is broken up over several frames. The difference is that this time, the garbage collection uses more time each frame, and requires fewer total frames to finish. This is because we adjust the time allotted to the garbage collection based on the remaining available frame time if Vsync or Application.targetFrameRate is being used. This way, we can run the garbage collection in time which would otherwise be spent waiting, and thus get garbage collection “for free”.

Enabling incremental garbage collection

Incremental garbage collection is currently supported on the following platforms:

  • Mac standalone player
  • Windows standalone player
  • Linux standalone player
  • iOS
  • Android
  • Windows UWP player
  • PS4
  • Xbox One
  • Nintendo Switch
  • Unity Editor

Note that incremental garbage collection is not currently supported on WebGL. Incremental garbage collection requires the .NET 4.x Equivalent scripting runtime version.

On supported configurations, Unity provides Incremental garbage collection as an option in the “Other settings” area of the Player settings window. Just enable the Use incremental GC checkbox.

Player Settings to enable incremental garbage collection
Player Settings to enable incremental garbage collection

In addition, if you set the VSync Count to anything other than Don’t Sync in your project Quality settings or with the Application.VSync property or you set the Application.targetFrameRate property, Unity automatically uses any idle time left at the end of a given frame for incremental garbage collection.

You can exercise more precise control over incremental garbage collection behavior using the Scripting.GarbageCollector class. For example, if you do not want to use VSync or a target frame rate, you could calculate the amount of time available before the end of a frame yourself and provide that time to the garbage collector to use.

インクリメンタルコレクションで起こりうる問題

ほとんどの場合、インクリメンタルガベージコレクションは、ガベージコレクションスパイクの問題を軽減することができます。しかし、いくつかのケースでは、インクリメンタルガベージコレクションは実際には有益ではないかもしれません。

インクリメンタルガベージコレクションが作業を中断するとき。これは、管理されているすべてのオブジェクトをスキャンして、どのオブジェクトがまだ使用されていて、どのオブジェクトがクリーンアップされるかを判断するマーキングフェーズを分割するものです。マーキングフェーズの分割は、作業のスライス間でオブジェクト間の参照がほとんど変化しない場合に有効です。オブジェクトの参照が変更された場合、それらのオブジェクトは次のイテレーションで再びスキャンされなければなりません。このように、あまりにも多くの変更があると、インクリメンタルガベージコレクタに負担がかかり、マーキングパスが終了しない状況が発生します。この場合、ガベージコレクションは、完全な非インクリメンタルコレクションを行うようになります。

また、インクリメンタルガベージコレクションを使用する場合、Unityは、参照が変更されたときにガベージコレクションに通知するための追加コード(ライトバリアと呼ばれる)を生成する必要があります(ガベージコレクションは、オブジェクトを再スキャンする必要があるかどうかを知ることができます)。これにより、参照を変更する際にオーバーヘッドが発生し、一部のマネージドコードでは測定可能なパフォーマンスへの影響があります。

しかし、ほとんどの典型的なUnityプロジェクト(「典型的な」Unityプロジェクトというものがあればの話ですが)では、ガベージコレクションのスパイクに悩まされている場合は特に、インクリメンタルガベージコレクションの恩恵を受けることができます。

ゲームやプログラムが期待通りに動作するかどうかを確認するには、必ずProfiler を使用してください。

追加情報

メモリ管理は巧妙で複雑なトピックであり、知識を獲得する労力も大きいものです。さらに詳しく学びたい場合は memorymanagement.org に素晴らしい情報が紹介されており、出版物やオンラン記事が一覧になっています。オブジェクトプールの詳細は Wikipedia ページ (英語)Sourcemaking.com を参照してください。


  • 2019–01–17
  • Mono と IL2CPP スクリプティングバックエンドでガベージコレクションを無効にする機能は [2018.3](https://docs.unity3d.com/2018.3/Documentation/Manual/30_search.html?q = newin20183) で追加 NewIn20183
  • Unity2019.1 で追加されたexerimental Incremental Garbage Collection機能を追加しました。 ニューイン20191
  • 追加のプラットフォームでIncremental Garbage Collectionのサポートを追加しました。PS4、XBox One、Nintendo Switch、Unity Editorに対応2019.2 ニューイン20192
イベント関数の実行順序
プラットフォーム依存コンパイル