Zenject Reflection Baking の効果測定

1. はじめに

こんにちは、クライアント開発部のちょろめです。

Zenject って便利ですよね。Zenject はバーチャルキャストの開発でも愛用されています(バーチャルキャストでは Zenject からフォークされた Extenject を使用しています)。

その Zenject ですが Reflection Baking という機能が存在することはご存じでしょうか。

Reflection Baking はランタイムでの Reflection にかかる処理時間を減らす Zenject の機能です。

今回は Zenject のサンプル Unity プロジェクトとバーチャルキャストの Unity プロジェクトで Reflection Baking を試してみました。

2. Zenject Reflection Baking とは

Zenject は Unity 向けの DI フレームワークです。Zenject における DI では Reflection が用いられます。

Reflection Baking はその Reflection にかかる処理時間を減らす Zenject の機能です。

代償なく高速化が実現する訳ではありません。ランタイムの Reflection がはやくなる代わりにビルド時間が伸びます。

ビルド中に Cecil というライブラリを使用して C# から生成されたアセンブリに対して IL ウィービング(?)をして、ランタイムで Zenject が行う処理を各クラスに直接埋め込むそうです(ナンモワカラン)。

これによって、ランタイムにおける Reflection によるコード分析のコストを静的メソッドを呼び出すコストに置き換えられるそうです。

また、IL2CPP ではついでにインスタンスの生成もはやくなるそうです。

詳細は README の Reflection Baking をご覧ください。

3. 調査環境

今回は以下の 2 つの Unity プロジェクトに Reflection Baking を適用してみました。

その他の環境は以下の通りです。

  • Unity Version : 2018.4.18f1
  • Zenject : Extenject 8.0.1
  • Scripting Backend : IL2CPP
  • C++ Compiler Configuration : Release
  • Target Platform : Windows - x86_64 - Development Build

4. 調査準備

4.1 Reflection Baking の有効化

Reflection Baking を有効にするための作業です。README の Reflection Baking にだいたい書いてあります。

  1. Project タブにて Create/Zenject/Reflection Baking Settings で Reflection Baking 設定用ファイルを作成
    • ZenjectReflectionBakingSettings.asset が作成されます
    • 配置する場所は Assets 以下ならどこでもいいです
  2. ZenjectReflectionBakingSettings 内の Namespace Patterns に Bake したいクラスを含んでいる namespace を追加
    • ^VirtualCast と書くと VirtualCast namespace とその子となる namespace 内にあるクラスがすべて bake 対象になります

正しく設定ができていればビルド時に以下のようなログがいくつか流れてきます。

4.2 Bake 中に発生するエラーの修理

Reflection Baking の効果を最大限発揮させるための作業です。それは bake 中に発生するエラーをすべてつぶすことです。

bake の途中でエラーが発生してもエラーの発生した assembly の bake が失敗するのみでビルドは通ります。実行ファイルも普通に動作します。

しかし、その assembly のクラスではランタイムで Reflection が行われます。

そのため、Reflection Baking の効果を最大限発揮することはできません。大きい assembly で bake に失敗すると特にです。

これにはなかなか気づけずハマりました。

バーチャルキャストの Unity プロジェクトではエラーがたくさん出ました。その Bake 中エラーの詳細については「9. おまけ」で紹介します。

5. 評価方法

Reflection Baking の効果をどう測定するかについてです。

今回は SceneContext と GameObjectContext に要する処理時間がどう変化するのかを見たかったので 2 つの指標を用意しました。

5.1 ZEN_INTERNAL_PROFILING による SceneContext のプロファイリング

SceneContext に要する処理時間を計測します。

README の Optimization Recommendations/Notes に書いてある方法を使いました。

Project Settings/Player/Scripting Define SymbolsZEN_INTERNAL_PROFILING を追加します。すると SceneContext が動くとき Console タブに以下のようなログが流れてきます。

Reflection Baking によってこの Type Analysis - Direct Reflection が処されるそうです。
今回はこのログの2つの値を記録します。

  • Context 全体の処理時間 : Total time tracked: [XXX.XX] ms.
  • Reflection のみの処理時間 : (XXXX ms) Type Analysis - Direct Reflection

5.2 オレオレ手法による GameObjectContext のプロファイリング

ZEN_INTERNAL_PROFILING では動的に生成される GameObjectContext が要する処理時間をみることができません。

そのため、以下のコミットのような修正を加えて GameObjectContext に用する時間も ZEN_INTERNAL_PROFILING と同じ仕組みでプロファイルできるようにしました。

https://github.com/chromee/zenject-sample/commit/397c3a5366f2cfbfe4bf48644ede27ba73a4b8a0

この修正では各 GameObjectContext が処理を終えるたびにログが流れますが、そのログには各 GameObjectContext が単体で要した処理時間が記載されている訳ではありません。

その GameObjectContext が処理されるまでに処理された全 GameObjectContext が要した処理時間の合計が記載されます。

ややこしいのですがこれしか方法が思いつかなかった…。

今回は各シーンごとで最後に流れてきた GameObjectContext のログを見ることにしました。

6. 評価結果

5で紹介した2つの指標を Reflection Baking ありの状態となしの状態のそれぞれで 5 回ずつ計測を行いました。

6.1 Zenject のサンプル Unity プロジェクト

Zenject のサンプル Unity プロジェクト では GameObjectContext が使用されていなかったので SceneContext の方の指標のみを計測しました。

結果を表、箱ひげ図にしたものがこちら。

Context bake
有無
1 回目 2 回目 3 回目 4 回目 5 回目
SceneContext 全体 baked 40.2 38.47 38.82 39.03 37.37
not baked 45.61 41.43 42.64 41.7 44.51
Reflection のみ baked 3 3 3 3 3
not baked 9 9 9 9 9

Reflection 単体、Context 全体どちらとして見ても処理時間が減っていることが分かりますね。

ちょうど Reflection の処理時間の減少分と同じぐらい Context 全体の処理時間が減ってるように見えます。

6.2 バーチャルキャストの Unity プロジェクト

バーチャルキャストの Unity プロジェクトで計測しました。

結果がこちら。全部の箱ひげ図を作るのはしんどかったので Scene B だけ箱ひげ図を作りました。

Context bake
有無
1 回目 2 回目 3 回目 4 回目 5 回目
SceneContext
全体 (Scene A)
baked 463.82 466.41 441.46 424.04 460.26
not baked 426.11 462.43 454.14 511.97 486.73
Reflection のみ baked 6 6 4 6 7
not baked 8 9 10 10 11
SceneContext
全体 (Scene B)
baked 130.73 131.42 132.23 129.77 133.62
not baked 140.19 139.24 140.24 146.13 143.86
Reflection のみ baked 6 6 6 6 6
not baked 12 12 12 12 13
GameObjectContext
全体 (Scene B)
baked 163.61 171.74 183.27 124.36 143.93
not baked 177.49 186.66 161.63 185.64 206.44
Reflection のみ baked 1 1 1 1 1
not baked 4 5 4 4 5
SceneContext
全体 (Scene C)
baked 7.58 7.05 7.33 7.27 7.29
not baked 7.5 7.62 7.56 7.71 8.25
Reflection baked 0 0 0 0 0
not baked 1 0 0 0 1
GameObjectContext
全体 (Scene C)
baked 996.64 1023.6 983.04 979.79 959.03
not baked 1009.09 1016.56 1037.66 1033.75 1098.84
Reflection のみ baked 2 2 2 2 2
not baked 14 14 15 14 14

いずれのシーンでもどちらの指標も Bake 後の方が減少していることが確認できました。

Reflection の減少幅に比べて Context 全体の減少幅がやたら大きかったです。SceneContext と GameObjectContext の両方で。

Reflection Baking によるインスタンス生成速度の向上が聞いているのだろうか 🤔

なんにせよ Reflection Baking によってはやくなっていることが確認できました。

8. おまけ

8.1 バーチャルキャストの Unity プロジェクトにおける bake 時エラーとその対策

ここではバーチャルキャストの Unity プロジェクトで bake 時に発生したエラーとその対策について紹介します。

8.1.1 エラーその1

  • 現象

    Inject する必要のないメソッドに Inject attribute をつけるとこのエラーが発生するようです。

  • 対策

    不必要な Inject 属性を消しましょう。

8.1.2 エラーその2

  • 現象

    このエラーは bake 中に出るものではないのですが一応紹介しておきます。

    このエラーは IL2CPP の処理中に発生するので、 Mono ビルドだと発生しないのですが、 IL2CPP ビルドだとビルドが通らなくなります。

    これは Inject メソッドの返り値が void 以外だと発生するようです。

  • 対策

    Inject メソッドの返り値は void にしましょう。

8.1.3 エラーその3

  • 現象

    このエラーは bake 対象のクラスがコンストラクタ、または Inject メソッドでデフォルト引数に bool を使用していると以下のようなエラーが出ます。

    また、このエラーは bool に限らず、string, int, enum 以外の型をデフォルト引数として使用すると発生するようです。

  • 対策

    • その1

    該当するコンストラクタを持つクラスに [NoReflectionBaking] 属性をつけます。 こうすることでそのクラスが bake に含まれなくなります。

    というのが正攻法だとは思うのですが、実際に該当するクラスをすべて見つけて [NoReflectionBaking] をつけるのは簡単ではありませんでした。数が多いのと、ビルドしてみないと全部潰せたか分からないからです。

    • その2

    Zenejct の ReflectionBakingModuleEditor.cs を修正してしまいます。

    下のコミットのような修正を加えることで floatbool をデフォルト引数に使用しても bake が失敗しなくなります。

    https://github.com/chromee/zenject-sample/commit/433b0655f802d908dc69b50de05b89539b8fcb40

    今回の調査ではこちらの方法を採用しました。

9. おわり

書き終わってから気づいたのですがビルド時間がどれくらい伸びるかもみとけばよかったなと思いました。体感ではビルド時間がそんなに伸びてる感じはしませんでした。

Zenject の Reflection Baking についてネット上に全然情報がなくて今回の調査はかなり苦労しました。

最初、 Reflection Baking は動いてるっぽいけど全然パフォーマンスが上がらなくて首を傾げたのですが、色々試したらちゃんとパフォーマンスが上がってよかったです。

とはいえ今回の調査では各所で数 ms はやくなる程度でした。

しかし、README 曰くとある Unity プロジェクトでは Zenject の起動時間が 45% 短縮されたそうなので試してみる価値はあると思います。たぶん。

是非お手元のプロジェクトでも Reflection Baking を試してみてはいかがでしょうか。

10. 参考リンク