Unity 2020.2 で適用予定の最適化が複数ベータ版でテスト向けに公開されています。この記事では、大きな高速化が期待できる領域と、これらの改善を実現するために行ったことの内側についてご紹介します。
パフォーマンスの高いコードを書くことは、効率の良いソフトウェア開発に不可欠な要素であり、Unity では常に開発プロセスの一部となっています。2 年前、私たちは思い切って専任の最適化チームを結成し、パフォーマンスそれ自体を機能の 1 つと考えて重点的に取り組むことにしました。Unity 2020.2 リリースで適用予定の最適化の概要は、本記事の残りをお読みください。また、Unity 2020.2 ベータ版のリリースノートもご参照ください。
最適化チームは、この機能のオリジナル開発者であるシーン管理チームと密接に協力して、以下のようなネスト状のプレハブのさまざまな最適化を行いました。
Prefab のインスタンスを読み込むとき、元の Prefab アセットと比較して、インスタンス内で変更があったプロパティに修正を適用します。これが PropertyModification です。PropertyModification をマージする際には、動的配列への更新や挿入があり、この構造体は非常に大きいため、すぐにコストが大きくなります。新しい Property 配列から PropertyModification を消去するのではなく、すでに更新されたプロパティを追跡することで、このメソッドは 60 倍速度が向上しました(テストプロジェクトで 3,300ms から 54ms まで)。
PropertyModification を更新する際に、修正リストが正しくソートされない場合があり、新しいソートアルゴリズムが求められていました。以前は、ソートは古い変更を新しい配列に並べ替えることで行われていました。テストプロジェクトでは、これに 11 秒かかりました。代わりにコンテナー内でソートを行い、変更点を選び出すようにしたことで、ソート時間は 44 ミリ秒に短縮されました(250 倍速)。また、修正が必要なかった場合は、11 ミリ秒(800 倍速)まで短縮されました。
さらに、修正のリストの中の propertyPath の検索は、コンテナーをハッシュセットに変更することで、50 倍速(300ms から 6ms)になりました。これにより、プロパティの差分の生成が全体的に最適化されました。
データベースのスケーラビリティテストでは、エディターの ScriptedImporter で、登録されるインポーターの数が増えると、登録機能のスケーリングが悪くなるという結果を得ました。インポーターをファイル拡張子別に辞書に格納することで機能を最適化し、競合する場合の検索を高速化しました。全体的な最適化としては、100 から 5,000 のインポーターを処理した場合に、12 から 800 倍以上の高速化が達成されるという結果を得ました(全体的な改善結果については、右のグラフを参照)。
チームは、1 フレームだけしか存在しない文字列について、多数使われていた遅い文字列のメモリ割り当てを一時メモリラベルに置き換えました。実際には、Unity のほとんどの文字列は、1 つの関数呼び出し内のローカル変数としてしか存在しません。そのため、高速なメモリアロケーターを使用することができます。現在、ほとんどの文字列ユーティリティ関数は一時メモリを使用しています。
この作業の一部は 2020.1 ですでに取り込まれています。以下に、この作業によって削除された遅い文字列割り当ての数を示すグラフをいくつか示します。このグラフは、さまざまなプロジェクトで 2020.1.0a12 から 2020.2.0a20 までの間に、数回のイテレーションによる改善を重ねて、遅い文字列割り当ての数がどのように変化したかを示しています(x 軸はイテレーション回数、y 軸は文字列割り当ての数)。
もう 1 つのエディターワークフローの最適化は、FindReferencesInScene です。以前は、プロジェクトビューでアセットを右クリックして、メニューからシーン内の参照の検索を行うと、大きなシーンで時間がかかることがありました。
過度なスマートポインターのデリファレンスを回避し、一時メモリを活用することで、一般的なユースケースにおいて、約 10% の高速化を実現しました。
シーンの参照を見つけられない場合、それらのスマートポインターをデリファレンスしようとすると、毎回ファイルシステムから無効なファイル名を使って読み込みを試みてしまっていました。無効なファイル名を検出し、読み込みに失敗することがわかっているファイルを開くようにファイルシステムに要求する挙動を抑制することで、検索時間を短縮しました。検索のスピードは最大 3 倍に高速化されました。
社内の他のチームと連携しながら JobQueue の最適化に取り組んできました。今年の初めに、DOTS Sample プロジェクトのプロファイリングから着手し、その結果、AtomicStack::Pop() で予想外に高いコストが発生していることがはっきりしました。さらに調査を進めると、問題は JobQueue のメモリ管理システム、特にアイテムのメモリ管理プールとして AtomicStack を使用していた JobInfo のメモリ管理システムにあることがわかりました。
データ指向技術スタック(DOTS)では、メモリ割り当てのために ForEach の要素ごとに Pop() を要求し、また、メモリ解放のために要素ごとに Push() を要求する ForEach ジョブが存在します。これは、AtomicStack の head アイテムの競合につながります。
Unity の別のチームは、ForEach ジョブの要素ごとの Pop() を回避するために、1 つの操作として要素のチャンク割り当てをサポートする、メモリ管理のユースケースに特化した新しいアトミックコンテナを実装しました。
初期のローカル性能テストの結果は、ジョブワーカーのスレッド数増加に伴い、最大 2 倍のパフォーマンス向上を示すという、非常に心強いものでした。
DOTS チームのメンバーから、新しいコンテナーがパフォーマンス上のメリットを示すであろうユースケース、すなわち JobQueue ForEach ジョブについての指摘がありました。
このサンプルは Android で動作させたときの結果です。緑が新しいコード、赤は古いコードです。
これまで、Camera.main の使用は検索を行うため、原則推奨されない手法でした。以前は、タグを持つすべてのゲームオブジェクトが事前に検索され、一致するタグを持つゲームオブジェクトが一時配列に引き出されていました。その後、引き出された一時配列が検索され、有効になっている Camera コンポーネントを持つオブジェクトがあれば、それが返されるという仕組みでした。
新しいアプローチでは、MainCamera タグを持つオブジェクト専用のリストを保存し、マッチする可能性のあるオブジェクトを格納した二次的な配列を使いません。その代わりに、専用のリストに直接問い合わせ、一致するものが見つかればすぐにそれが返されます。この時検索されるオブジェクトはすべて MainCamera タグを持つオブジェクトとなります。そのため、検索が成功する確率はこれまでの方法よりはるかに高くなります。
50,000 個のオブジェクトを含んだ人工的なテストケースでは21,000 から 51,000 倍の高速化が確認されました。
の中で スポットライトチームの顧客プロジェクト(下図)では、今回の改善によって、何百ミリ秒もの処理時間がゼロになりました。
以前は、カメラが RenderManager クラスに追加されたり、RenderManager クラスから削除されたりするたびに、リンクトリストが更新され、アクティブなカメラが深度順に並べ替えられていました。変更のたびに、各カメラの深度をチェックするためにメモリの割り当てとポインターのデリファレンスが必要で、カメラが多くなると時間がかかる場合がありました。
現在、リストは順序付けが必要な場合にのみソートされます。これはレンダリングのみがソート済みのリストを気にするプロセスだからです。そのため、ロード中にカメラを単なる配列に追加または削除することができ(アロケーションが少なくて済む)、ソートはソート済みのリストが最初に要求された時(つまり、レンダリング中)にのみ実行されます。このテストでは、最終的なタイミングでパフォーマンスが向上していることを示しています(右端のオレンジ色のバーが新しいコードの数値を示す)。
テクスチャ読み込み時のヒッチを減らすために 2D テクスチャの作成をグラフィックスのスレッドからワーカースレッドに移動しました。Unity 2019 のリリースで、ほとんどのグラフィックスバックエンドに対してこの最適化が適用されました。Unity 2020.2 では DirectX 12 の追加ケースについて修正を行い、8K テクスチャについて発生していた 80 ミリ秒停止する問題を除去しました。
Unity 2020.1 では、テクスチャのスウィズル処理をオフラインに移動し、GPU メモリに直接読み込むことで、コンソールでの Texture2D の読み込みを最適化しました。この最適化でパフォーマンスは最大 30% 向上しています。向上幅はテクスチャのサイズとプラットフォームにより変動します。
Unity 2020.2 ではさらに、コンソールでのキューブマップの読み込みを最適化しました。2K のキューブマップで、一部のコンソールではジョブスレッドで 30 ミリ秒の時間短縮が達成される例が見られ、個別のテクスチャの読み込みにかかる時間は全体で最大 15 ミリ秒短縮されました。
私たちは常に、プラットフォーム固有のプロファイリングツールと Unity 独自のカスタムプロファイラーを組み合わせて使ったプロファイリングと分析に基づいて、パフォーマンス最適化の方針を定めています。プロファイリングの取り組みを支援するために、私たちは Profile Analyzer ツールを作成しました。このツールが、Unity 2020.2 では検証済みパッケージとして提供されます。
Profile Analyzer 1.0.0 と 1.0.x のアップデートでは、開発をスムーズにするためのバグ修正、いくつかのパフォーマンスの最適化、およびいくつかの小さな機能追加が行われます。
プロファイラー開発チームは、今後のツール開発もリードしていきます。
これらのアップデートは、Unity で継続的に行われているパフォーマンス向上への貢献の一部に過ぎません。2021 年は、さらに多くのパフォーマンス向上を実現するために、すでに懸命な取り組みが行われています。引き続きフィードバックをお寄せください。また、コメント欄に注力してほしい分野があればお知らせください。
Unity 2020.2 の開発を通じて、2020 年の注力分野としていた、パフォーマンス、安定性、ワークフローの改善を継続して行っています。ぜひベータ版プログラムにご参加いただき、Unity 2020.2 ベータ版フォーラムで今後のアップデートについてのご意見をお聞かせください。
Is this article helpful for you?
Thank you for your feedback!