Version: Unity 6.0 (6000.0)
言語 : 日本語
Emscripten 用のウェブネイティブプラグイン
ウェブのキャッシュ動作

Unity Web のメモリ

Unity Web ではメモリの制約により、実行するコンテンツの複雑さが制限されることがあります。

Web コンテンツはブラウザー上で動作します。ブラウザーは、アプリケーションがコンテンツを実行するために必要なメモリを、そのメモリスペースに確保します。利用可能なメモリの量は、以下の条件によって異なります。

  • 使用するデバイス
  • 使用する OS
  • 使用するブラウザーと、それが 32 プロセッサーと 64 プロセッサーのどちらで動作するか
  • ブラウザーの JavaScript エンジンがコードを解析するのに必要なメモリ量
  • ブラウザーがタブごとに別々のプロセスを使用するかどうか、コンテンツが他のすべての開いているタブとメモリスペースを共有する必要があるかどうか。

注意Web のメモリに関するセキュリティリスクについては、セキュリティとメモリリソースを参照してください。

Unity Web のメモリ使用量

Unity Web コンテンツの以下の領域では、ブラウザーが大量のメモリを割り当てる必要があります。

Unity のヒープ

Unity はメモリヒープを使用して、すべての Unity エンジンランタイムオブジェクトを格納します。これには、マネージオブジェクト、ネイティブオブジェクト、ロードされたアセット、シーン、シェーダーなどが含まれます。これは、他のプラットフォームで Unity Player が使用するメモリのようなものです。

Unity のヒープは、割り当てられたメモリの連続したブロックです。Unity は、アプリケーションのニーズに合わせて、ヒープの自動サイズ変更をサポートしています。ヒープサイズは、アプリケーションが実行すると拡張され、最大 2 GB まで拡張できます。Unity はこのメモリーヒープを Memory オブジェクトとして作成します。Memory オブジェクトの buffer プロパティはサイズ変更可能な ArrayBuffer で、WebAssembly コードがアクセスするメモリの生のバイトを保持します。

ヒープの自動リサイズは、ブラウザーがアドレス空間の連続したメモリブロックの割り当てに失敗すると、アプリケーションがクラッシュする原因となります。このような理由から Unity のヒープサイズをできるだけ小さくしておくことが重要です。したがって、アプリケーションのメモリ使用量を計画する際には注意が必要です。Unity のヒープサイズをテストしたい場合は、プロファイラーを使って、メモリブロックの内容をプロファイルすることができます。

Web Player SettingsMemory Growth Mode オプションを使用して、ヒープの初期サイズと増加を制御できます。デフォルトのオプションは、デスクトップのすべてのユースケースで動作するように構成されています。ただし、モバイルブラウザーの場合は、高度な調整オプションを使用する必要があります。モバイルブラウザーの場合は、Initial Memory Size をアプリケーションの一般的なヒープ使用量に設定することをお勧めします。

アセットデータ

Unity Web ビルドを作成すると、Unity は .data ファイルを生成します。このファイルには、アプリケーションの起動に必要なシーンとアセットがすべて含まれています。Unity Web は実際のファイルシステムにアクセスできないため、仮想メモリのファイルシステムを作成し、ブラウザーはこの .data ファイルをここで解凍します。Emscipten フレームワーク (JavaScript) は、このメモリファイルシステムをブラウザーのメモリ空間に確保します。コンテンツが実行されている間、ブラウザーのメモリは解凍されたデータを維持します。ダウンロード時間とメモリ使用量の両方を低く抑えるために、この圧縮されていないデータをできるだけ小さくするようにしてください。

メモリ使用量を削減するために、アセットデータを AssetBundle にまとめることができます。アセットバンドルを使うと、アセットのダウンロードを完全に制御することが可能になります。つまり、アプリケーションがアセットをダウンロードするタイミングや、ランタイムがアセットをアンロードするタイミングを制御できます。未使用のアセットをアンロードすると、メモリを解放できます。

AssetBundles は、Unity のヒープに直接ダウンロードします。そのため、ブラウザーが余分な割り当てをすることはありません。

Data Caching を有効にすると、ユーザーのマシンにコンテンツのアセットデータがキャッシュされます。これにより、後の実行時にそのデータを再ダウンロードする必要がなくなります。Unity Web ローダーは、IndexedDB API を使用してデータキャッシングを実装しています。このオプションにより、ブラウザーがネイティブにキャッシュするには大きすぎるファイルをキャッシュすることができます。

データキャッシングは、ブラウザーがアプリケーションデータをユーザーのマシンに保存することを可能にします。ブラウザーでは、しばしば、キャッシュに保存できる量やキャッシュできる最大ファイルサイズが制限されます。これでは、アプリケーションがスムーズに動作するためには十分ではありません。IndexedDB API を使用した Unity ウェブローダーキャッシングを使用すると、Unity はブラウザーキャッシュの代わりに IndexedDB にデータを保存できます。

データキャッシングオプションを有効にするには、File > Build Settings > Player Settings > Publishing Settings の順に移動します。

ガベージコレクション

ガベージコレクションとは、使われていないメモリを探し出して解放するプロセスです。Unity のガベージコレクションの仕組みの概要については、自動メモリ管理を参照してください。ガベージコレクションのプロセスをデバッグするには、Unity Profiler を使用します。

WebAssembly のセキュリティの制限により、ユーザープログラムは悪用を防ぐために、ネイティブの実行スタックを調査することが許可されていません。

つまり、Web プラットフォームでは、マネージコードが実行されていないとき (潜在的にライブの C# オブジェクトをリファレンスする可能性があるとき) にのみ GC を実行できます。これは、レンダリングされたゲームフレームが終了するたびに発生します。

つまり、Web プラットフォームでは、ガベージコレクターは C# コードの実行中に実行できず、各プログラムフレームの終了時にのみ実行されます。このような不一致により、他のプラットフォームと比較して、ウェブでのガベージコレクション動作にいくつかの違いが生じます。

このような違いがあるため、フレームごとに多くの一時割り当てを実行するコードには細心の注意を払ってください。特に、これらの割り当てで直線的かつ連続したサイズの増加が見られる場合は注意してください。このような割り当ては、ガベージコレクターに一時的なクアッドメモリ増加圧力を引き起こす可能性があります。

例えば、長時間実行されるループがある場合、以下のコードはウェブ上で実行すると失敗する可能性があります。これは、ガベージコレクターが for ループの反復の間に実行されないためです。この場合、ガベージコレクターは中間の文字列オブジェクトが使用するメモリを解放できずに、Unity のヒープのメモリを使い果たしてしまう可能性があります。

string hugeString = "";
 
for (int i = 0; i < 100000; i++)
{
   hugeString += "foo";
}

上記の例では、ループ終了時の hugeString の長さは 3 * 100000 = 300000 文字です。ただし、コードは最終的な文字列を生成する前に 10 万個の一時的な文字列を生成します。ループ全体で割り当てられた合計メモリは 3 * (1 + 2 + 3 + … + 100000) = 3 * (100000 * 100001 / 2) = 15 GB になります。

ネイティブプラットフォームでは、ループの実行中にガベージコレクターが文字列の以前の一時的なコピーを継続的にクリーンアップします。そのため、上記のコードを実行するのに合計 15 GB の RAM は必要ありません。

Web プラットフォームでは、ガベージコレクターはフレームの終了まで一時的な文字列コピーを回収求しません。その結果、上記のコードは 15 GB の RAM を割り当てようとするとメモリ不足になります。

以下のコードは、このタイプの一時的な 2 次メモリ不足が発生する 2 番目の例を示しています。

byte[] data;
 
for (int i = 0; i < 100000; i++)
{
   data = new byte[i];
   // do something temporary with data[]
} 

ここでは、最後の 100 KB 配列のみが保持されていますが、コードは一時的に 1 + 2 + 3 + …+ 100000 バイト = 5 GB 相当のバイトを割り当てます。これにより、最終的な出力では 100 KB しか必要ないのに、Web プラットフォームのメモリが不足しているように見えます。

このような問題を制限するには、一時的メモリの割り当て量が二次関数的に増加するコード構成を回避する必要があります。代わりに、最終的に必要なデータサイズを事前に割り当てるか、List<T> または類似のデータ構造を使用して、幾何級数的に容量が増加する予約を実行し、一時的なメモリ不足を軽減します。

例えば、データ構造の最終的なサイズがわかっている場合は、List<T> コンテナで、必要な容量の事前割り当てを可能にする List<T>.ReserveCapacity() 関数の使用を検討してください。同様に、以前に数 MB のメモリを保持していたコンテナのサイズを縮小する場合は、List<T>.TrimExcess() 関数の使用を検討してください。

注意C# のデリゲートやイベント (DelegateActionFunc など) を使用する場合、これらのクラスは内部で上記のような線形的な増加の割り当てを使用します。Web プラットフォームのガベージコレクターの一時的なメモリ不足を最小限にするために、これらのクラスではフレームごとのデリゲートの登録や登録解除を過剰に行わないようにしてください。

追加リソース

Emscripten 用のウェブネイティブプラグイン
ウェブのキャッシュ動作