よく分からないまま使っていた部分があったので、PC/コンソール向けに挙動を検証してみました。
環境
- Unity 6.2
- Addressable 2.7.3
- Windows DevelopmentBuild
検証
まず、Scriptを用いて8Kテクスチャを3枚とMeshで約1GBのオブジェクトを生成し、それを6つ作成しました。
また、以下のようなコードを用意し、任意のオブジェクトをロード/アンロードできる環境を作成しています。
using UnityEngine;
using UnityEngine.AddressableAssets;
public class ObjectLoader : MonoBehaviour
{
const string KeyFormat = "Assets/GeneratedMeshes/GeneratedObject_{0:000}/GeneratedObject_{0:000}_Prefab.prefab";
const int KeyCount = 6;
private readonly Instance[] _instances = new Instance[KeyCount];
private void Awake()
{
var cam = Camera.main;
cam.transform.position = new Vector3(0, 10, 0);
cam.transform.rotation = Quaternion.Euler(90, 0, 0);
for (var i = 0; i < KeyCount; i++)
{
_instances[i] = new Instance(i);
}
}
private void OnGUI()
{
for (var i = 0; i < KeyCount; i++)
{
GUILayout.BeginHorizontal();
if (GUILayout.Button($"Load {i}"))
{
_instances[i].Load();
}
if (GUILayout.Button($"Unload {i}"))
{
_instances[i].UnLoad();
}
GUILayout.EndHorizontal();
}
}
private class Instance
{
private readonly string _key;
private bool _isLoading = false;
private bool _isLoaded = false;
private GameObject _gameObject;
private GameObject _instanceObject;
public bool IsLoaded => _isLoaded;
public Instance(int index)
{
_key = string.Format(KeyFormat, index);
}
public void Load()
{
if (_isLoaded || _isLoading) return;
_isLoading = true;
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
Addressables.LoadAssetAsync<GameObject>(_key).Completed += (obj) =>
{
_gameObject = obj.Result;
_isLoading = false;
_isLoaded = true;
_instanceObject = Instantiate(_gameObject);
Debug.Log($"Load {_key} completed in {sw.ElapsedMilliseconds}ms");
};
}
public void UnLoad()
{
if (!_isLoaded) return;
Addressables.Release(_gameObject);
_gameObject = null;
_isLoaded = false;
Destroy(_instanceObject);
_instanceObject = null;
}
}
}
順番にロードする場合
全アセットを1つのAddressableにまとめ、デフォルト設定のまま順番にロードしてみました。
上からポチポチロードしていくと
Load GeneratedObject_000_Prefab.prefab completed in 10514ms
Load GeneratedObject_001_Prefab.prefab completed in 328ms
Load GeneratedObject_002_Prefab.prefab completed in 256ms
Load GeneratedObject_003_Prefab.prefab completed in 283ms
Load GeneratedObject_004_Prefab.prefab completed in 322ms
Load GeneratedObject_005_Prefab.prefab completed in 561ms
- 初回ロード(000)が極端に遅い
- 以降は比較的高速
全部Unloadしたうえでもう一度やってもこれと同じような結果が出ます
メモリの挙動
LZ4圧縮では「ブロック単位で読み込み」が行われるため、初回に全Bundleを展開しているわけではありません。
メモリプロファイラで確認すると、ロード数に比例して使用量が増加しており、確かに逐次読み込みが行われています。
初回だけ遅い理由
となると何か一つ目が不思議ですね。プロファイラで見てみましょう
Profilerを確認すると、AssetBundleLoadFromAsyncOperation.IsCrc32Valid
が初回のみ大きく時間を消費していました。
全Unload後に再ロードしても再びスパイクが発生するため、適切にリソース管理しているほど頻繁にCRCチェックが走る ことになります。
CRCチェックを無効化
CRCチェックをGroup設定の方を変えてDisableにしてみましょう。
Load GeneratedObject_000_Prefab.prefab completed in 582ms
Load GeneratedObject_001_Prefab.prefab completed in 305ms
Load GeneratedObject_002_Prefab.prefab completed in 361ms
Load GeneratedObject_003_Prefab.prefab completed in 278ms
Load GeneratedObject_004_Prefab.prefab completed in 399ms
Load GeneratedObject_005_Prefab.prefab completed in 650ms
初回のスパイクは完全に消えました。
Remote配信時は検証が必要ですが、SteamやSwitchなどのコンソール向けでLocal利用が前提ならCRCチェックは不要
と言えそうです。
bundleのサイズに比例して時間が伸びます。
ランダムアクセスの場合
次に、非順序でロード/アンロードを試しました。
- 0~5全てロード
- 0以外アンロード
- 1~5をもう一度ロード
Load GeneratedObject_000_Prefab.prefab completed in 489ms
Load GeneratedObject_001_Prefab.prefab completed in 322ms
Load GeneratedObject_002_Prefab.prefab completed in 261ms
Load GeneratedObject_003_Prefab.prefab completed in 300ms
Load GeneratedObject_004_Prefab.prefab completed in 566ms
Load GeneratedObject_005_Prefab.prefab completed in 572ms
# ここで0以外アンロード
Load GeneratedObject_001_Prefab.prefab completed in 10ms
Load GeneratedObject_002_Prefab.prefab completed in 10ms
Load GeneratedObject_003_Prefab.prefab completed in 11ms
Load GeneratedObject_004_Prefab.prefab completed in 10ms
Load GeneratedObject_005_Prefab.prefab completed in 10ms
二回目が異常に早いです。
これは流石にインメモリに居そうなのでメモリプロファイラ見てみましょう
0以外アンロードしたケースと全ロードしたケースでメモリ使用量があまり変わらないのでメモリ上に残ってます。
同一Bundle内のものを一気に読み込むことは無いが、何もしないと一つずつは破棄してくれずに、Bundle内のすべてのものがアンロードされたタイミングでメモリ上から全開放してます。
全部常駐してもOKな単位でBundleを区切るというのは一つの目安になりそうです。
一つが大きなPrefabなのでbundle自体を分割する設定のほうが良いでしょう。
Load GeneratedObject_000_Prefab.prefab completed in 613ms
Load GeneratedObject_001_Prefab.prefab completed in 588ms
Load GeneratedObject_002_Prefab.prefab completed in 511ms
Load GeneratedObject_003_Prefab.prefab completed in 356ms
Load GeneratedObject_004_Prefab.prefab completed in 728ms
Load GeneratedObject_005_Prefab.prefab completed in 522ms
# 以下↑と同じ結果になるので割愛
シーケンシャルアクセス出来ないのと、別Bundleでアクセス効率が下がっているので100msぐらい平均で読み込み時間は長くなってますが、一つずつメモリ上から高速にアンロードされてます。
UnCompress
以下にUnCompressが最速と書いてますがどうでしょうか
https://docs.unity3d.com/ja/Packages/com.unity.addressables@1.20/manual/GroupSettings.html#asset-bundle-compression
Load GeneratedObject_000_Prefab.prefab completed in 774ms
Load GeneratedObject_001_Prefab.prefab completed in 522ms
Load GeneratedObject_002_Prefab.prefab completed in 561ms
Load GeneratedObject_003_Prefab.prefab completed in 500ms
Load GeneratedObject_004_Prefab.prefab completed in 1228ms
Load GeneratedObject_005_Prefab.prefab completed in 1133ms
今回は特殊に大きなファイルを入れてるというのはあるが遅くなった。今回のケースだとDecompressではなくIOがネックらしい(かなり早いm.2 SSD上に置いてます)
ケースバイケースなのかもしれないが、遅くなる可能性があるくらいなら基本LZ4から変えることは無さそう
Profiler
でPreloadManager
を見ると、ボトルネックが分かる。展開に時間かかってるケースがあればUnCompress
も試してみたら良いかも
まとめ(実務メモ)
- 初回スパイクの主因は CRC。ローカル運用前提なら CRC 無効化で大きく改善。リモート配信時は要要件検討
- 同一バンドル内は “束” で生存する。個別に生殺与奪したいならバンドル分割を。逆に「常駐前提の塊」はまとめる
- 圧縮は基本 LZ4(デフォルト)で開始。I/O vs CPU のどちらが詰まっているかを Profiler で見て、必要なら Uncompressed も試す