20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

サムザップAdvent Calendar 2022

Day 18

ロード時間を短くするために効率よくチューニングする

Last updated at Posted at 2022-12-17

本記事は、サムザップ Advent Calendar 2022 の12/18の記事です。

例えばよくあるゲームでローディング画面を開いて次のゲームシーンを待っている時間、絶対短くしたいですよね。

しかしながら、開発を続けていると、うっかりロードが長くなってしまったりすることもあると思います。
この記事では、ロード処理を改善することで、ロード時間を短くするためのチューニングテクニックを紹介したいと思います。

※ この記事で出てくるprofileのサンプルは、Unity2021.3.11f1を使用しています

ロード処理を無駄なく最大効率で行う

ロード時間はアセットの容量に比例して伸びます。
アセット量がボトルネックなら、処理をいくら工夫してもロード時間を早くする術はありません。
ところが、CPUがロード処理に対して十分に仕事をできておらず、結果としてロード時間が延びるということもあり得ます。
そこで、まずは 本当に毎フレーム時間いっぱいロード処理をやれているのか という観点でProfiler (CPU Usage Profiler Module) を眺めてみるのがよいと思います。

計測をする

さて、まずは理想の状態をイメージしてもらうために、たくさんのアセットバンドルを並列ロードしてる良さげなフレームを撮ってみました。

image.png

アセットバンドルのロード処理はPreload Manager というスレッドで行われているようです。

image.png

このPreloadManagerスレッドの欄にたくさん処理が書かれていれば、アセットバンドルのロード処理が休みなく行われていることになります。

逆に、十分なアセットのロード処理が発行していないフレーム (下図) をみてみると、PreloadManagerの処理のほとんどは Semaphore.WaitForSignalとなっていて、ただ待機してるだけになっています。

image.png

ロード画面においては、とにかくロード処理を短時間で行うことが望ましいので、PreloadManagerスレッドがたくさん働いている状態を作ると良いと言えます。

AssetBundleのロード処理を効率よく並列化する

ロードによる待機時間の短縮の大方針は、とにかく並列にロード命令を出してPreloadManagerが常に働いている状態を作る です。

具体的には

await LoadItem1Async();
await LoadItem2Async();

これを

await UniTask.WhenAll(LoadItem1Async(), LoadItem2Async());

こうしていく作業をひたすらやり切る です。

ロード開始直後に全部のロード命令を一気に出してロード自体を並列処理させることができれば、無駄なくロードができてかなり速くなると思います。

とはいえ、実際に開発中のプロジェクトでロード処理を改善したい場面に直面した時には、全てをがむしゃらに書き換えていくのはなかなか難しいかもしれません。
シンプルに工数がかかりますし、場合によってはデータの持ち方を変えたり初期化順を調整したりと、バグにつながるような変更もたくさん必要になると思います。
さらに、アセット一つ大変な思いで対応したとしても、ボトルネックが他のアセットだったりすれば効果を体感しづらいこともありえます。

ボトルネックになってるアセットを見つけ出して並列化することで効率的にチューニングができるといいですよね。
そこで、Asset Loading Moduleを活用します。

Asset Loading Module でどのフレームでどのアセットがロードされているかを知る

Asset Loading Moduleは Unity2021.2以降で使える機能でして、どのフレームでどのアセットが読み込まれたのかがわかります。
画像は、あるフレームでLoad中のアセットデータの一覧が表示されている様子です。

image.png

また、ひと手間かかるのですが、該当アセットのアセットバンドルがどのタイミングでロードされているのかもわかります。
こんな感じに注目したいアセットを開いて、謎のファイル名を覚えておく。
image.png

表示モードを変更して、All Framesに変更

上部の表示マーカーを右クリックして、First Frame Indexを表示する項目に加えてあげて、

検索窓に先程の謎のFile名を入力すると、
スクリーンショット 2022-12-16 15.33.19.png

該当ファイルのアセットバンドルがどのフレームでロードされて、いつアセットが取り出されたのかがわかります。
Typeの欄を確認すると、AssetBundleをロードしたのか、アセットをロードしたのかがわかり、該当処理のフレームはFirstFrameIndexをみるとわかります。
このスクショだと、Index 45でアセットバンドルがロードされて、65でstatus_410というアセットがロードされていることがわかります。

また、このFirstFrameIndexの値は、プロファイラの持つフレームのIndexを示しており、下図の Frame: 52/300の部分とリンクしています。

ただし、例えばIndexなので(?)、Index45なら、Frame46を見ないと該当データが存在しないので注意してください。
image.png

Asset Loading Module を活用して効率的に並列化する

並列化したロード処理は、ソースコード上では、(ものすごく簡単化すると)こんなイメージです。

async UniTask LoadAsset(string assetBundleName, string assetName)
{
    var filePath = GetAssetPath(assetBundleName);
    var assetBundle = await AssetBundle.LoadFromFileAsync(filePath);
    await assetBundle.LoadAssetAsync(assetName);
}

// 並列化したロード処理はこんなイメージ
// UniTask.WhenAll(LoadAsset("hoge", "hoge"), LoadAsset("fuga", "fuga"), ...);

まずはAssetBundleLoadの命令がUnity側の伝えられ、それらが終わって初めてAssetをAssetBundleからロードする処理がよばれることになります。
実際、Asset Loading Moduleを眺めていると、アセットバンドルロード命令を同じフレームで並列実行したものについては、すべてのAssetBundleロード処理が走ってから、アセットのロード処理が動いていそうでした。

image.png
例えば、このフレームまではアセットバンドル Typeばかりですが、

image.png
次のフレームからはAsset In AssetBundle Typeも発生しており、アセットバンドルからアセットをロードする処理が動いていることがわかります。

この特性を利用すると、アセットバンドルからアセットをロードする処理が動き始めてから、途中でAssetBundleロードが差し込まれるようなら、そのアセットは並列ロードできていないかもしれない と疑うことができます。

さらに、このアセットバンドルロードが断続的に起きてるようなら、もしかしたらこんな感じになってるかもせず、ロード処理を長引かせがちなので要注意です。

for(int i = 0; i < count; i++)
{
   await items[count].AssetLoadAsync();
}

また、ロード処理の最後の方には特に注意を払うべきです。
特に、最後の方にアセットロード量が少ないケース。終盤にアセットバンドルロードが飛んでいる可能性が高く、もしそれだけを待っているなら、並列化によってロードタイミングを早めることでロード待機時間を減らすことができます。

明らかに後半に少しずつロード命令が飛んでいるProfile結果の例を出しておきます。
image.png
Asset Loading Module Windowのグラフ上のロード後半付近に着目すると、僅かなロード量しか計上されていません。
詳細をみると、ロード後半にもかかわらずアセットバンドル Typeのロードが入っていることがわかります。

バックグラウンドスレッドの設定を変えてロード速度をアップ

PreloadManagerスレッドにギチギチに処理を詰め込んだところで、バックグラウンドスレッドの優先度をあげてロード速度をアップします。

Application.backgroundLoadingPriorityで設定を変更することで実現できます。
こちらの設定のみを変えて、同じシーンファイルのロード時間を比較してみました。

image.png

Application.backgroundLoadingPriority = ThreadPriority.High

image.png

Application.backgroundLoadingPriority = ThreadPriority.Low

同じシーンファイルをロードしているフレームにフォーカスしていますが、Low 257ms に対して、High 116ms と、2倍以上の速度でロードができていることがわかります。
効果が高いので是非試してみてください。

おわりに

ロード時間を短くするために効率よくチューニングするテクニックについて紹介しました。

  • ロード並列化を効率よく行うためのProfilerの利用方法
  • Application.backgroundLoadingPriorityによるロード処理の高速化

本記事で紹介した手法は多くのプロジェクトで使える知見だと思うので、ロード時間を短縮したいと思ったら是非試してみてください。

※ 本記事ではロード処理の最適化にフォーカスした話をしましたが、ロード時間の短縮にはアセット自体の最適化も重要です。
これらについてはUnityPerformanceTuningBibleが参考になると思います。興味があればこちらもぜひどうぞ。

明日は @norimatsu_yusuke さんの記事になります. お楽しみに。

20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?