Order of Execution for Event Functions
プラットフォーム依存コンパイル

Understanding Automatic Memory Management

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

Value and Reference Types

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

パラメーター渡しのときに直接格納されてコピーされる型は値型と呼ばれます。これらは整数、浮動小数点数、論理型、および Unity の構造体型(例えば、 Color および __Vector3__)も含みます。ヒープに割り当てられて、ポインタを通してアクセスされる型は参照型と呼ばれ、変数に格納されている値はあくまでも実際のデータへの “参照” です。参照型の例にはオブジェクト、文字列、および配列が含まれます。

Allocation and Garbage Collection

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

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

Unity uses the Boehm–Demers–Weiser garbage collector, a stop-the-world garbage collector. Whenever Unity needs to perform garbage collection, it stops running your program code and only resumes normal execution when the garbage collector has finished all its work. This interruption can cause delays in the execution of your game that last anywhere from less than one millisecond to hundreds of milliseconds, depending on how much memory the garbage collector needs to process and on the platform the game is running on. For real-time applications like games, this can become quite a big issue, because you can’t sustain the consistent frame rate that smooth animation require when the garbage collector suspends a game’s execution. These interruptions are also known as GC spikes, because they show as spikes in the Profiler frame time graph. In the next sections you can learn more about how to write your code to avoid unnecessary garbage-collected memory allocations while running the game, so the garbage collector has less work to do.

Optimization

Garbage collection is automatic and invisible to the programmer but the collection process actually requires significant CPU time behind the scenes. When used correctly, automatic memory management will generally equal or beat manual allocation for overall performance. However, it is important for the programmer to avoid mistakes that will trigger the collector more often than necessary and introduce pauses in execution.

いくつかの悪名高いアルゴリズムにより、一見では問題ないようにみえて 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;
    }
}

ここで鍵となるポイントは、新しい部分が文字列の正しいところにひとつひとつ追加されないということです。実際にループするたびに行われることは、line 変数の前回の中身が消滅します - もとの部分に新しい部分を加えたまったく新しい文字列が格納されます。文字列は 1 づつ値が増加されるため、消費されるヒープの量もまた増加し、この関数がコールされるたびに数百バイトの空きヒープ領域を容易に占有します。もし多くの文字列を連結するとき、Mono ライブラリの System.Text.StringBuilder クラスを使用したよりよい例があります。

しかし、繰り返しの連結ですら、頻繁にコールされない限りそこまでの問題を引き起こしませんが、Unity でその頻繁となりうる要因は暗黙的にはフレーム更新です。次のような場合です。

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public GUIText scoreBoard;
    public int score;
    
    void Update() {
        string scoreText = "Score: " + score.ToString();
        scoreBoard.text = scoreText;
    }
}

… このコードで各 Update がコールされるたびに新しい文字列が割り当てされ、新しいガベージが常に生成されます。そのほとんどはスコアが変更されたときのみテキストを更新することで節約できます。

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public GUIText 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;
        }
    }
}

これにより単に既存の配列の中身を新しい値で置き換えます。これにより初期割り当てがコールする元のコードの中で行われることが必要となるにも関わらず(ややエレガントでなくみえますが)、関数はコールされるときに新しいガベージを生成することはなくなります。

Disabling garbage collection

If you are using the Mono or IL2CPP scripting backend, you can avoid CPU spikes during garbage collection by disabling garbage collection at run time. When you disable garbage collection, memory usage never decreases because the garbage collector does not collect objects that no longer have any references. In fact, memory usage can only ever increase when you disable garbage collection. To avoid increased memory usage over time, take care when managing memory. Ideally, allocate all memory before you disable the garbage collector and avoid additional allocations while it is disabled.

For more details on how to enable and disable garbage collection at run time, see the GarbageCollector Scripting API page.

You can also try an experimental, incremental garbage collection option.

Requesting a Collection

As mentioned above, it is best to avoid allocations as far as possible. However, given that they can’t be completely eliminated, there are two main strategies you can use to minimise their intrusion into gameplay.

Small heap with fast and frequent garbage collection

このアプローチがベストであるのは、長期間のゲームプレイにおいて、スムーズなフレームレートを実現することが主な関心事であるゲームの場合などです。このようなゲームは一般的に小さなブロックを頻繁に割り当てしますが、これらのブロックは短期間しか使用されません。iOS でこのアプローチをとるときの典型的なヒープサイズは 200KB ぐらいであり、iPhone 3G において 5ms ほどかかります。もしヒープが 1 MB に増加すると、コレクションは 7ms かかります。このため定期的なフレームのインターバルでガベージコレクションを要求することにメリットがあります。厳密にはこれは一般的にコレクションを必要以上に発生させますが、素早く処理されゲームプレイへの最小の影響で済みます。

if (Time.frameCount % 30 == 0)
{
   System.GC.Collect();
}

しかし、このテクニックは注意して使用すべきで、プロファイラー統計をチェックして、本当に自身のゲームでコレクション時間を削減していることを確認すべきです。

Large heap with slow but infrequent garbage collection

このアプローチがベストであるのは、メモリ割り当て(ひいてはコレクション)が相対的に頻繁でなく、ゲームプレイのポーズの際にハンドリングできます。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();

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

Reusable Object Pools

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

Incremental Garbage Collection (Experimental)

Note: This is a preview feature and is subject to change. Any Projects that use this feature may need updating in a future release. Do not rely on this feature for full-scale production until it is officially released.

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

With incremental garbage collection enabled (above), the same project keeps its consistent 60fps frame rate, as the garbage collection operation is broken up over several frames, using only a small time slice of each frame (the darker green fringe just above the yellow Vsync trace).

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 Mac, Windows and Linux Standalone Players and on iOS, Android and Windows UWP players. More supported platforms will be added in the future. Incremental garbage collection requires the new .NET 4.x Equivalent scripting runtime version.

On supported configurations, Unity provides Incremental garbage collection as an experimental option in the “Other settings” area of the Player settings window. Just enable the Use incremental GC (Experimental) 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.

Possible problems with incremental collection

In most cases, incremental garbage collection can mitigate the problem of garbage collection spikes. However, in some cases, incremental garbage collection may not prove beneficial in practice.

When incremental garbage collection breaks up its work, it breaks up the marking phase in which it scans all managed objects to determine which objects are still in use and which objects can be cleaned up. Dividing up the marking phase works well when most of the references between objects don’t change between slices of work. When an object reference does change, those objects must be scanned again in the next iteration. Thus, too many changes can overwhelm the incremental garbage collector and cause a situation where the marking pass never finishes because it always has more work to do – in this case, the garbage collection falls back to doing a full, non-incremental collection.

Also, when using incremental garbage collection, Unity needs to generate additional code (known as write barriers) to inform the garbage collection whenever a reference has changed (so the garbage collection will know if it needs to rescan an object). This adds some overhead when changing references which can have a measurable performance impact in some managed code.

Still, most typical Unity projects (if there is such a thing as a “typical” Unity project) can benefit from incremental garbage collection, especially if they suffer from garbage collection spikes.

Always use the Profiler to verify that your game or program performs as you expect.

Experimental status

Incremental garbage collection is included in Unity 2019.1 as an experimental preview feature. This has been done for a number of reasons: * It isn’t yet supported on all platforms. * As outlined in the “Possible problems with incremental collection” section above, we expect incremental garbage collection to be beneficial or at least not detrimental performance-wise for most Unity content, and this seems to have been true for various projects we have been testing with. But as Unity content is very diverse, we want to make sure that this assumption stays true across the greater Unity ecosystem, and we need your feedback on this. * The requirement for Unity code and scripting VM (mono, il2cpp) to add write barriers to inform the garbage collection whenever references in managed memory have changed introduces a potential source of bugs where we have missed adding such a write barrier, which could lead to objects being garbage collected when they are still needed. Now, we have done extensive testing (both manual and automated) and we aren’t aware of any such issues, and we believe that this feature is stable (otherwise, we would not ship it). But, once again, because of the diversity of Unity content and because such bugs might turn out to be hard to trigger in practice, we cannot completely rule out the possibility that there may be issues.

So, overall we believe that this feature works as expected and there are no known issues with it. But, because of the complexity of the Unity ecosystem, we need some time and exposure to get the confidence to drop the experimental label, which we will do based on the feedback we get.

Further Information

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


  • 2019–01–17
  • Ability to disable garbage collection on Mono and IL2CPP scripting backends added in Unity 2018.3 NewIn20183
  • Added exerimental Incremental Garbage Collection feature added in Unity 2019.1 NewIn20191
Order of Execution for Event Functions
プラットフォーム依存コンパイル