Search Unity

機能プレビュー:インクリメンタルガベージコレクション

, 11月 26, 2018

Unity 2019.1a10 に、実験的な新機能であるインクリメンタルガベージコレクション(インクリメンタル GC)が追加されました。この記事では、この機能の概要、プロジェクトに対するメリット、今後の計画、および皆さんからのフィードバックの重要性について説明します。

インクリメンタル GC を使う理由

C# 言語は、マネージドメモリと自動ガベージコレクションを使用します。つまり、メモリ内のオブジェクトの追跡や、不要になったオブジェクトに割り当てられているメモリの解放を自動で行います(詳細はこちらのドキュメントをご覧ください)。そのメリットは、メモリの追跡と不要なメモリの解放がガベージコレクターによって自動的に行われるので、一般に開発者がメモリを手動で追跡/解放する必要がないという点です。その結果、作業の負荷が減り、潜在的なバグが発生する大きな原因もなくなります。デメリットは、ガベージコレクターの処理にはある程度時間がかかることと、開発者が望まないタイミングでガベージコレクターの処理が発生する可能性があることです。

Unity では、Boehm–Demers–Weiser ガベージコレクターという Stop the World ガベージコレクターの一種を使用しています。この種のガベージコレクターは、ガベージコレクションの実行が必要になると、プログラムの実行を停止し、ガベージコレクションが完了してから、通常のプログラム実行を再開します。そのため、プログラムの実行中に、どこかのタイミングで遅延が発生する可能性があります。この遅延時間は、ガベージコレクターが処理に必要とするメモリ量とプログラムを実行しているプラットフォームによって異なりますが、おおむね 1 ミリ秒未満~数百ミリ秒の範囲です。当然ながら、ゲームなどのリアルタイムアプリケーションにとって、遅延は非常に大きな問題となります。プログラムの実行がガベージコレクターによって勝手に中断させられると、滑らかなアニメーションに必要な一定のフレームレートを維持することができなくなるからです。このような割込みは、滑らかなプロファイラーのフレーム時間グラフに急な山形(スパイク)として表示されるため、GC スパイクとも呼ばれます。通常、開発者はこの問題を回避するために、ゲーム実行中に「ガベージ」メモリが作成されることのないようにコードを記述して、ガベージコレクターによる処理があまり発生しないようにしますが、常に問題を回避できるわけでも、簡単に回避できるわけでもありません。

そこで活用していただきたいのが、インクリメンタルガベージコレクション(インクリメンタル GC)です。インクリメンタル GC も Boehm–Demers–Weiser GC を使用しているという点は変わりませんが、実行が段階的に行われるため、作業が複数のスライスに分割されるという点が異なります。インクリメンタル GC では、通常の GC のようにプログラムの実行を長時間にわたって一旦中断するのではなく、短時間の中断を複数回発生させます。そのため GC 全体が高速化することはありませんが、複数のフレームに負荷を分散させることで、アニメーションの滑らかさを損なう GC スパイクの問題を大幅に軽減できます。

その効果をお見せするために、Unity プロファイラーのスクリーンショットをご用意しました。いずれも、ちょっとした GC パフォーマンスのテスト用スクリプトを macOS スタンドアロンビルドとして実行した結果を示したものですが、インクリメンタル GC を無効にしているか、有効にしているかが異なります。スクリプトは、60 fps をキープして実行されています。フレームの水色の部分は「スクリプト実行」(スクリプトの System.Threading.Thread.Sleep 呼出しによってシミュレーションしたもの)で、黄色の部分は Vsync (次のフレームの開始を待機)、暗い緑色の部分はガベージコレクションです。

インクリメンタル GC を使用しなかった場合(上記)、プロジェクトでは数秒ごとに約 30ms のスパイクが発生し、そこで滑らかな 60fps フレームレートのグラフが途切れています。

同じプロジェクトでインクリメンタル GC を使用した場合(上記)、GC が複数のフレームに分割され、各フレームの小さなタイムスライスのみが消費されるので、60fps の一定のフレームレートを維持しています。

このスクリーンショットは同じプロジェクトのもので、同様にインクリメンタル GC を有効にして実行していますが、今回は 1 フレームの「スクリプト実行」の数を少なくしてあります。ここでも、GC の処理が複数のフレームに分割されています。しかし、以前よりも GC が各フレームで使う時間が長くなっており、終了までに必要な合計フレーム数が少なくなっています。このようなことが起こる理由は、Vsync または Application.targetFrameRate を使用していると、残りの利用可能なタイムフレームに基づいて、GC に割り当てる時間を調整できるからです。これにより、待機に充てられる時間で GC を実行できるので、GC が実質的に「無償」になります。

インクリメンタル GC を有効にする方法

現在、インクリメンタル GC は、Unity 2019.1 alpha において、Mac、Windows、および Linux のスタンドアロンプレイヤー、iOS、Android、および Windows UWP のプレイヤーでサポートされています。サポートされるプラットフォームは、今後も追加していく予定です。インクリメンタル GC を使用するには、新しい .NET 4.x Equivalent スクリプティングランタイムバージョンが必要です。

サポートされている設定では、「Player」設定ウィンドウの「Other Settings」エリアで、実験的なオプションとしてインクリメンタル GC を使用できます。試すには、「Use incremental GC (Experimental)」チェックボックスをオンにして、プレイヤーをビルドするだけです。

2019.1 に追加された新しい Scripting.GarbageCollector API を使用すると、インクリメンタル GC の動作をより正確に制御できます。

プロジェクトでテストしたら、うまくいったかどうかをフォーラムでお聞かせください。フィードバックをお待ちしています。

期待される成果

インクリメンタル GC を有効にすると、ガベージコレクターが、ガベージコレクションの作業を複数の処理に分割します。この処理は、複数のフレームに分散させることができます。GC スパイクが問題となっていたほとんどのケースでは、インクリメンタル GC を利用することで、問題の軽減が期待できます。しかし、Unity で作られるコンテンツは非常に多岐にわたっており、挙動もさまざまであるため、インクリメンタル GC が役に立たないケースも存在する可能性があります。

具体的には、インクリメンタル GC によって作業が分割されると、分割されたうちのマーキングフェーズでは、使用中のオブジェクトを追跡するために、すべてのマネージドオブジェクトをスキャンしてそのオブジェクトが参照している他のオブジェクトを特定します。そのとき、オブジェクト間の参照のほとんどは、作業のスライス間で変化しないと想定されます。参照が変化した場合、変化したオブジェクトは次のイテレーションで再度スキャンする必要があります。これにより、作業が追加され続けて、インクリメンタルコレクションがいつまでも終了しない状況が発生する可能性があります。その状況が発生した場合、GC は、フォールバックしたうえで非インクリメンタルで完全なコレクションを実行します。インクリメンタル GC のパフォーマンスが非インクリメンタル GC に劣る状況は、参照を常時変更する人工的なテストケースを作成すると簡単に作り出すことができます。

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

しかし、一般的な Unity プロジェクト(仮に「一般的」な Unity プロジェクトというものがあるとすれば)のほとんどは、インクリメンタル GC を使うと、特に GC スパイクに困っている場合は、恩恵を受けることができます。

実験的機能として提供中

インクリメンタル GC は、Unity 2019.1 では実験的なプレビュー機能として含まれています。このようになっているのには、以下のようないくつかの理由があります。

  • 一部のプラットフォームでは、この機能がまだサポートされていません。
  • 上の「期待される成果」セクションで説明したように、インクリメンタル GC は、Unity で作成したコンテンツのほとんどではメリットがある、または少なくともパフォーマンスに悪影響を及ぼさないと考えられます。また、さまざまなプロジェクトをテストしたところ、テストした範囲ではそれを裏付ける結果が得られています。しかし、Unity で作られるコンテンツは多岐にわたるため、広大な Unity エコシステムにおいて、私たちの想定が正しいのかどうかを確認する必要があります。この点については、お客様からのフィードバックもお待ちしています。
  • マネージドメモリ内の参照が変更された場合に GC に通知するためのライトバリアを追加するという Unity コードと VM(mono、il2cpp)のスクリプティングの要件は、ライトバリアの追加漏れというバグが発生する原因となります。ライトバリアを追加しないと、まだ必要なオブジェクトがガベージコレクションされてしまう恐れがあります。広範なテスト(手動、自動の両方)を完了した現在も、そのような問題は見つかっていないので、この機能は安定していると考えています(そうでなければ、リリースしていません)。しかし、繰り返しになりますが、Unity で作られるコンテンツは多岐にわたっており、実際問題としてこのようなバグを意図的に発生させるのは難しいため、問題が存在する可能性を完全に排除できません。

総じて言えば、この機能は期待どおりの効果を発揮し、既知の問題はないと考えられます。しかし、Unity エコシステムは複雑なので、ある程度の期間は実験的な機能として皆さんに公開し、寄せられたフィードバックに基づいて、確信が得られたら「実験的」のラベルを外す予定です。

今後の計画

インクリメンタル GC は現在 Unity 2019.1 のアルファ版で利用可能ですが、2019 年には、この機能にいくつかの変更を加える予定があります。

  • 他のすべてのプレイヤープラットフォームに対するサポートを追加する
  • (いただいたフィードバックの内容次第で)「実験的」ラベルを削除する
  • インクリメンタル GC モードでエディター自体を実行するサポートを追加する
  • (いただいたフィードバックの内容次第で)インクリメンタル GC をデフォルトのオプションにする

評判の良い他の GC を採用しなかった理由

インクリメンタル GC について説明すると、Boehm GC の代わりに Xamarin の Sgen GC などの他の GC ソリューションの使用を検討しなかった理由についてよく質問を受けます。(独自の GC を作成することも含め)他のオプションはこれまでに検討済みで、今後も検討を続ける予定ですが、引き続き Boehm GC を使用したうえでインクリメンタルモードを採用することにした主な理由は、それが大きな改善を実現できると同時に最も安全な手段であると考えられるからです。上の「実験的ステータス」セクションで説明したように、リスクの 1 つにライトバリアの欠如や不適切な配置によるバグの発生があります。しかし、Unity が現在使用している GC ソリューションよりも新しいソリューションの大部分で、ライトバリアを追加する要件が依然として残されたままになっています。そのため、Boehm GC から変更しないことで、ライトバリアを正しく追加しなければならないことで生じるリスクを、まったく別の GC に切り替えることで生じるリスクからある程度分離できます。

Unity では引き続き、この分野の発展を観察し、インクリメンタル Boehm の導入によってユーザーのニーズがどのように変化するかに注目していきます。インクリメンタル Boehm を導入しても、多くのユーザーが GC スパイクなどの問題に苦労している状況が変わらない場合は、別のオプションを検討します。

そのため、繰り返しになりますが、皆さんからのフィードバックが重要です。ぜひアルファ版をご確認のうえ、フォーラムにご意見をお寄せください。

20 コメント

コメントの配信登録

コメント受付を終了しました。

  1. Guys, this is a blessing!

  2. Non-progressive GC was of course a huge idea.
    Do not observe any rights that may help you develop the text and other things. Fighting!

  3. Unity’s GC is non-compacting, correct? Would a possible improvement be the ability to force-compact when there’s plenty of time to do so (like loading), reducing memory fragmentation issues over long playtimes?

  4. Chillersanim

    11月 29, 2018 6:16 pm

    So, what happens if I try to create large objects that overflow the remaining memory (assuming there’s enough garbage to offer the required memory)?
    You can’t just make my constructor call incremental as well.
    Will it fallback to instant GC or is memory regularly collected to prevent this from happening?

    1. Jonas Echterhoff

      12月 3, 2018 9:09 am

      It should fall back to doing a full GC if needed.

  5. I’ve had many issues with GC. I cringed every time I saw someone play my game and it would pause for a second once every minute.

  6. Heck yes. This will be a great addition. Seems like an obvious choice to make this the standard as long as GC is manually triggrable through scripts for the moments when you need it to clear completely.

  7. Finally!!! Excellent news! This will make our life so much easier.

  8. I wonder how big that overhead is… Do you perhaps have some measurements of references overhead?

    1. Jonas Echterhoff

      11月 27, 2018 11:05 am

      Well, the overhead always depends on what you are doing (and the platform). If you only look at the specific operation of assigning a reference in managed code that does become several times slower. But the question is how much of the total time in your game is spent assigning references in managed code? Outside of micro-benchmarks, I did not find noticeable performance degradations in Unity projects. But that does not mean that there won’t be any, as each project is unique.

  9. Good write up.
    This is an exciting feature

  10. Robert Cummings

    11月 26, 2018 4:39 pm

    Sounds awesome. One issue with allocations I have is with multi scene loading. Async streaming a scene in is never pretty and I hope this will smooth out the final jerks I can’t really do much about without source access.

    1. Jonas Echterhoff

      11月 26, 2018 7:29 pm

      I’m not sure it will help with that. Scene loading performs a lot of work in native code, GC should not necessarily be the problem there. But you should be able to tell what time is being spent on using the profiler.

  11. Glad to hear the garbage collector is finally getting this update, however I really hope Unity can add some profiling tools to debug situations which cause the incremental GC to fail.

    1. Jonas Echterhoff

      11月 26, 2018 7:28 pm

      What do you mean by “cause the incremental GC to fail”? It is not expected to “fail”. Or do you mean cases where incremental GC will still not result in the performance you need or fall back to a full GC causing spikes? In such cases, you will need to reduce the amount of garbage created, as before. Incremental GC can help better distribute the time for GC, but it does not fundamentally change the work which needs to be done. So creating garbage is still not “free”. But the tools to help you reduce garbage exit – the Unity profiler can help you track allocations.

  12. YES! You finnally do this! Stable frame time very important for engine!

  13. Alan Mattano

    11月 26, 2018 2:47 pm

    WOWOW finally no more spikes and smooth fps… !!! Looks promising

    1. Alan Mattano

      11月 26, 2018 3:48 pm

      How to minimize GC allocation?

      1. InNoHurryToCode

        12月 10, 2018 9:00 pm

        By creating less instances of classes, gameobjects, etc. Everything that is created via the new keyword is memory on the heap, thus managed by the CG. One way is to use an object pool, another one is to create everything at the beginning of the game to reduce cg spikes.

        Google, ask around on the forums and many more people will be able to help you.