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 を適用してみました。
- Zenject-WithSampleGames@v8.0.1.unitypackage に含まれる SampleGame2 (Advanced)
- バーチャルキャストの Unity プロジェクト
その他の環境は以下の通りです。
- 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 にだいたい書いてあります。
- Project タブにて
Create/Zenject/Reflection Baking Settings
で Reflection Baking 設定用ファイルを作成ZenjectReflectionBakingSettings.asset
が作成されます- 配置する場所は Assets 以下ならどこでもいいです
- ZenjectReflectionBakingSettings 内の
Namespace Patterns
に Bake したいクラスを含んでいる namespace を追加^VirtualCast
と書くとVirtualCast
namespace とその子となる namespace 内にあるクラスがすべて bake 対象になります
正しく設定ができていればビルド時に以下のようなログがいくつか流れてきます。
1 2 3 4 5 |
Added reflection baking to '4' types in assembly 'Sample.dll', took 0.03 seconds UnityEngine.Debug:Log(Object) Zenject.ReflectionBaking.ReflectionBakingBuildObserver:TryWeaveAssembly(String) (at Assets/Plugins/Zenject/OptionalExtras/ReflectionBaking/Unity/ReflectionBakingBuildObserver.cs:102) Zenject.ReflectionBaking.ReflectionBakingBuildObserver:OnAssemblyCompiled(String, CompilerMessage[]) (at Assets/Plugins/Zenject/OptionalExtras/ReflectionBaking/Unity/ReflectionBakingBuildObserver.cs:37) UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr) (at C:/buildslave/unity/build/Modules/IMGUI/GUIUtility.cs:179) |
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 Symbols
に ZEN_INTERNAL_PROFILING
を追加します。すると SceneContext が動くとき Console タブに以下のようなログが流れてきます。
1 2 3 4 5 6 7 8 9 10 |
SceneContext.Awake detailed profiling: Total time tracked: 3104.19 ms. Details: 67.2% (02960x) (2086 ms) User Code 19.4% (01243x) (0602 ms) Type Analysis - Direct Reflection 06.1% (02928x) (0189 ms) DiContainer.Resolve 02.3% (00003x) (0071 ms) Other 02.3% (00259x) (0071 ms) DiContainer.Bind 01.3% (01112x) (0041 ms) DiContainer.Instantiate 00.7% (01032x) (0023 ms) Searching Hierarchy 00.4% (01243x) (0011 ms) Type Analysis - Calling Baked Reflection Getter 00.3% (01852x) (0010 ms) DiContainer.Inject |
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
-
現象
1234567InvalidOperationException: Sequence contains more than one elementSystem.Linq.Enumerable.SingleOrDefault[TSource] (System.Collections.Generic.IEnumerable`1[T] source) (at <fbb5ed17eb6e46c680000f8910ebb50c>:0)Zenject.ReflectionBaking.ReflectionBakingModuleEditor.TryFindLocalMethod (Zenject.ReflectionBaking.Mono.Cecil.TypeReference specificTypeRef, System.String methodName, Zenject.ReflectionBaking.Mono.Cecil.TypeReference& declaringTypeRef, Zenject.ReflectionBaking.Mono.Cecil.MethodReference& methodRef) (at Assets/Plugins/Zenject/OptionalExtras/ReflectionBaking/Common/ReflectionBakingModuleEditor.cs:599)~Inject する必要のないメソッドに Inject attribute をつけるとこのエラーが発生するようです。
-
対策
不必要な Inject 属性を消しましょう。
8.1.2 エラーその2
-
現象
1234IL2CPP error for method 'System.Void [該当クラス]::**zenInjectMethod0(System.Object,System.Object[])' in assembly '[該当クラスのある assembly]'Additional information: Attempting to return a value from method 'System.Void [該当クラス]::**zenInjectMethod0(System.Object,System.Object[])' when there is no value on the stack. Is this invalid IL code?このエラーは bake 中に出るものではないのですが一応紹介しておきます。
このエラーは IL2CPP の処理中に発生するので、 Mono ビルドだと発生しないのですが、 IL2CPP ビルドだとビルドが通らなくなります。
これは Inject メソッドの返り値が void 以外だと発生するようです。
-
対策
Inject メソッドの返り値は void にしましょう。
8.1.3 エラーその3
-
現象
123ZenjectException: Cannot process values with type 'bool' currently. Feel free to add support for this and submit a pull request to github.このエラーは bake 対象のクラスがコンストラクタ、または Inject メソッドでデフォルト引数に bool を使用していると以下のようなエラーが出ます。
また、このエラーは bool に限らず、string, int, enum 以外の型をデフォルト引数として使用すると発生するようです。
-
対策
- その1
該当するコンストラクタを持つクラスに
[NoReflectionBaking]
属性をつけます。 こうすることでそのクラスが bake に含まれなくなります。というのが正攻法だとは思うのですが、実際に該当するクラスをすべて見つけて
[NoReflectionBaking]
をつけるのは簡単ではありませんでした。数が多いのと、ビルドしてみないと全部潰せたか分からないからです。- その2
Zenejct の
ReflectionBakingModuleEditor.cs
を修正してしまいます。下のコミットのような修正を加えることで
float
とbool
をデフォルト引数に使用しても bake が失敗しなくなります。https://github.com/chromee/zenject-sample/commit/433b0655f802d908dc69b50de05b89539b8fcb40
今回の調査ではこちらの方法を採用しました。
9. おわり
書き終わってから気づいたのですがビルド時間がどれくらい伸びるかもみとけばよかったなと思いました。体感ではビルド時間がそんなに伸びてる感じはしませんでした。
Zenject の Reflection Baking についてネット上に全然情報がなくて今回の調査はかなり苦労しました。
最初、 Reflection Baking は動いてるっぽいけど全然パフォーマンスが上がらなくて首を傾げたのですが、色々試したらちゃんとパフォーマンスが上がってよかったです。
とはいえ今回の調査では各所で数 ms はやくなる程度でした。
しかし、README 曰くとある Unity プロジェクトでは Zenject の起動時間が 45% 短縮されたそうなので試してみる価値はあると思います。たぶん。
是非お手元のプロジェクトでも Reflection Baking を試してみてはいかがでしょうか。
10. 参考リンク
- Tag
- Unity