こんにちは、Unityエンジニアのオオバです。

Unityでキーワードを追加して処理を分岐させながらシェーダーを書いていくと、知らず知らずのうちにシェーダーバリアントが大量に増えることがあります。シェーダーバリアントを作成するためには#prgama multi_compileまたは#pragma shader_featureを追加だけなのでとても簡単です。

以前multi_compileについて執筆したので参考にどうぞ。
Unityシェーダーのマクロとマルチコンパイル

シェーダーバリアントが増えることによってコンパイルしたシェーダーが占有するメモリが増えていきます。本記事ではシェーダーバリアント増加によるメモリ使用量の計測について説明していきいます。

👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

multi_compileとshader_featureの違い

前述した通りmulti_compileとshader_featureはシェーダーバリアントを作成するために使用します。

という大きな違いがあります。
基本はshader_featureでシェーダーバリアントを作成した方が生成される個数は最小限に抑える事ができそうです。

Uniteでのポストモーテムセッションや技術ブログを読むと、最適化する上でmulti_compileからshader_featureに置き換える話を見かけます。

そして、ここからが本題です。

シェーダーバリアントが最大でどのくらいのメモリを使用するかを把握する必要性がある

shader_featureで指定されているシェーダーバリアントが、本来どのくらい生まれるかは開発、運用してみないと分かりません。

なぜなら前述した通り、ビルド時に使用しているキーワードの組み合わせしか作成されないためです。運用を開始してからシェーダーを改修するのは難易度が非常に高くなるため、開発中(せめて中盤頃)にコンパイルしたシェーダーが占めるの最大使用メモリは決めておきたいところです。

もう少し詳しく説明すると、集団開発する上でシェーダーはエンジニア以外も触ります。Unityエンジニアが想定する範囲内での使用なら問題ないですが、例えばクリエータがそういった暗黙の決まりを知らずにシェーダーを使ってしまう場合もあります。
すると、いつの間にかシェーダーバリアントが爆増して、メモリ使用量が増えてアプリが落ちたり、またシェーダーのコンパイルに依るスパイク(カクツキ)が問題になったりします。
※スパイクについては本記事では割愛

以上の理由からUnityエンジニアはシェーダーバリアントの影響範囲を把握しておくべきだと考えます。

一時的にshader_featureをmulti_compileに置き換えてみる

どうやってシェーダーバリアントの影響範囲を把握するかですが、一時的にshader_featureをmulti_compileに置き換えて検証してみます。

こうすることで全てのシェーダーバリアントを作成することができます。この状態でコンパイルしたシェーダーが占めるメモリを計測して、そのシェーダーのポテンシャルを測ります。

計測方法

Unityエディタ上で実行するプロファイラの値にはノイズが入りすぎててあてにならないので、必ず実機を使って計測するようにしています。具体的にはUnityエディタのProfilerに実機(iPhone6s)を繋ぎ、その中のMemoryProfilerで確認します。
※PackageからインストールするMemoryProfilerではありません

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_0

Window > Analysis > Profilerから起動します。

MemoryProfilerのグラフ

シェーダーコンパイル前

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_1

シェーダーコンパイル後

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_2

Total Allocatedの値が大きく変化しています。
このビューでは詳しいことは分からないので詳細を見ていきます。

MemoryProfilerのSimpleビュー

シェーダーコンパイル前

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_3

シェーダーコンパイル後

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_4

UnityProfiler => MemoryProfilerのSimpleビューで確認すると、Unityが管理するネイティブメモリが40MBほど増えている事が分かります。

Detailビューで更に詳しく見ていきます。

MemoryProfilerのDetailビュー

シェーダーコンパイル前

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_5

シェーダーコンパイル後

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_6

Other > Rendering > ShaderLabの値が約40MBほど増加しています。コンパイルしたシェーダーはネイティブメモリとして計上されているようです。

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_7

Otherが何者なのかについては以下のリファレンスを参考にどうぞ。
Memory プロファイラーモジュール - Unity マニュアル

サードパーティMemoryProfilerでは確認できない

ここは余談になります。

UnityのMemoryProfilerに2種類あります。
Profiler付属製とPackageManager経由でインストールするサードパーティ製です。

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_8

サードパーティ製のMemoryProfilerはこのようにグラフィカルにメモリ分布を表示してくれてとても見やすいのですが、コンパイルしたシェーダーのメモリ使用量は表示してくれないようなので注意です。

念のためにXcodeで確認

Unityエディタでのプロファイリングでほぼほぼ事足りますが、計測した値が正しいかXcodeでのプロファイリングもしてみます。

シェーダーコンパイル前

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_9

シェーダーコンパイル後

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_10

約57MB増えています。Unityエディタで計測する値と一致はしませんが、これが実際にこのアプリで使用されているメモリということになります。そこまで大きく値がずれていないという事が確認できました。

Xcode付属のプロファイラInstrumentsを使用すれば更に詳しく確認できると思いますが、今回は時間の関係上調査しません。

シェーダーバリアントが多く作られるシェーダーの最大使用メモリを計測する_11

Unityエディタ上では気づかなかったのですが、シェーダーコンパイル時に一瞬ピークメモリが跳ね上がるのも気になります。
こちらも同様Instrumentsで確認すれば何かしら要因が見えてくるかもしれません。

最後に

multi_compile、shader_featureとシェーダーバリアントという機能はとても便利です。フルスクラッチでシェーダーをこれらのパターン書く事を考えると本当に神機能だと思います。

ただ神機能も調子にのって使いすぎるとシェーダーバリアントが大量に作られてしまい、メモリを大量に使用してしまっていたという事も発生します。

最大でどのくらいメモリを消費するかを把握し、アプリ全体のピークメモリを制御する事で安全に開発を進められたらと思います。また日々実機でMemoryProfilerの値をチェックして異常な値になっていないかをウォッチする事も大事。

オススメ記事
検証環境
参考サイト