エフアンダーバー

個人開発の記録

【Unity】Stripping Levelについて

前回の記事を書いた後で自分の書いたことが本当にあっているのか心配になってきたのでいろいろと調べなおしてみました。 わかったような気になっていたけど結構わかっていないもんで・・・ そして結局この記事の結論としてもようわからんかったという内容なのですが、せっかく調べたので書いておきます。

前回の記事

www.f-sp.com

Stripping Levelとは?

Stripping LevelとはiOSやAndroidといったモバイル向けの最適化オプションです。
Edit > Project Settings > Player からプレイヤー(ゲームの実行エンジン)設定を開き、 iOSまたはAndroidのタブでOther Settingsカテゴリ内のOptimizationサブカテゴリに項目があります。 最新のUnityを利用している場合には項目が見当たらないかもしれませんが、 その場合にはOther Settingsカテゴリ内のConfigurationサブカテゴリにあるScripting Backendの項目を"Mono2x"に設定すると出てきます *1。 Scripting Backendについては記事の最後に補足します。

モバイル端末ではダウンロード時間や端末の容量の問題でアプリ自体の容量を小さく保つ必要があります。 現在は緩和されてきていますが、あまりに容量の大きいものはストア(AppStoreやGooglePlay等)に提出できません。 そのためUnityでは不要なコードを除去することでアプリサイズを小さくする方法を提供しています。 このコード除去の度合いを決定するのがStripping Levelになります。

Stripping Levelで選択できる値は以下の4つになります。

  • Disabled
  • Strip assemblies
  • Strip ByteCode
  • Use micro mscorlib

Disabled(無効)はその名の通り不要コードの除去を一切行いません。 デフォルト値はこれになっています。

Strip assembliesはゲームに含まれるスクリプトコードを解析して参照しているクラスやメソッドを割り出し、 使用していないクラスやメソッドのコードを除去します。

Strip ByteCodeはStrip assembliesによるコード除去を行ったうえで、残りのコードをメタデータのみを残してすべて除去します。 もちろん、これはせっかく記述したコードを完全に取り除くわけではありません。 C#やUnityScriptで記述されたコードは一度IL(Intermediate Language)と呼ばれる中間言語へと変換され、 その後、機械語へと変換されて実行されます。 Strip assembliesの段階ではこの中間言語と機械語のコードの両方を含んでいて冗長です。 そこで、Strip ByteCodeではこのうち直接実行されない中間言語のコードをメタデータのみを残して除去します。

Use micro mscorlibは特別なmscorlibを用いたうえでStrip ByteCode相当のコード除去を行います。 mscorlibはSystem.Object型やSystem.String型など.NET Framework(Unityの開発基盤)において中心的役割を果たすアセンブリ(ライブラリ)です。 Use micro mscorlibではこれからUnityの実行に必要のないクラスやメソッドを除いた特別なバージョンのmscorlibを使用します。

どれを選択すればいい?

注意 以降の内容は適当な資料が見つからなかった、あるいは私の知識が乏しく資料が理解できなかったために多分の推測が含まれています。 鵜呑みにせず、参考程度にお読みください。

さて、複数のオプションが存在するということはそれぞれに適した状況もまた存在するはずです。 我々は何を考慮して、どのオプションを選択すればいいのでしょうか? オプション値はDisabled < Strip assemblies < Strip ByteCode < Use micro mscorlibの順に単純にコードの除去量が増えるため、 これらの境がどこにあるのかを考えてみます。

Strip assembliesの判断基準

まずはDisabledとStrip assembliesの使い分けについて。
これはマニュアルにもしっかりと記述されており、リフレクションを利用しているかどうかです。 後述しますが、より正確にはリフレクションを多用しているかどうかになります。

リフレクションとはコードのメタデータを利用したプログラミングテクニックで、 例えば、メソッド名を文字列(String型)で指定してのメソッド実行や、 クラスに含まれるフィールドの列挙などに使用します。 非常に柔軟なコードが書けるという利点がある一方で、 開発支援ツールとは非常に相性が悪く、静的解析の際には必ずといっていいほど障害となります。 というのも、オブジェクト(文字列など)の値を基に参照するクラスやメソッドが決まるために、 オブジェクトの値を追跡する必要があるためです。 オブジェクトの値は次々に変換および伝播していくため追跡は非常に困難なのです。

さて、Strip assembliesではスクリプトコードを解析して参照しているクラスやメソッドを特定するのでした。 ここで、リフレクションを利用した参照をしていると解析の際に参照していることを検出できません。 そのため必要のないクラスやメソッドだと判断されて除去されてしまいます。 そして、実行時になって初めて参照先が存在しないということで例外が発生して落ちます *2

この節の最初に判断基準について「より正確にはリフレクションを多用しているかどうか」だといいました。 というのも、いちおう対応策があるためです。 link.xmlというファイルにリフレクションによって参照しているクラスやメソッドを記述しておくと、 それらは除去の対象から外されます。 詳細はマニュアルを参照してください。

というわけで、参照しうる対象が把握できないほどリフレクションを多用している場合を除いて、 Stripping LevelはStrip assemblies以上にしてよさそうです。

ちなみにこのリフレクションですが、 実行速度が非常に遅いことから特別な場合を除いてゲーム開発ではあまり推奨されません。 特別な理由がないのであれば使わないようにしましょう。

Strip ByteCodeの判断基準

次はStrip assembliesとStrip ByteCodeの使い分けについて。
個人的に最もわからないのがこの二つの使い分け。 そもそもビルド結果の中間言語のコードが何に使われているのかが謎 (何故つねに除去してしまわないのか)。

一応考えられる使い分けとしてはビルド時間の短縮でしょうか? 全てのメソッドのコードを除去して新しいアセンブリを作成するため、Strip ByteCodeではビルド時間が若干長くなります。 開発時のデバッグではコードサイズが小さい必要はあまりないため、ビルド時間が短いほうが好ましいです。 しかし、ビルド時間を気にするのであればDisabledでいいわけでStrip assembliesの存在意義がよくわかりません・・・

基本的にはStrip assembliesの要件を満たすのであれば、 Strip ByteCodeにしておいた方がいいように思えます。

ただし、もしかするとビルド結果の中間言語のコードは実行時に使われており、 Strip ByteCodeにすることで実行できなくなるコードが存在するのかもしれません。
もし知っている方がいたら教えてください m(_ _)m

Use micro mscorlibの判断基準

最後にStrip ByteCodeとUse micro mscorlibの使い分けについて。
これは単純にmicro mscorlibで除かれるクラスやメソッドを利用するか否かです。 ただ通常はUse micro mscorlibを使うと決めてから、 除かれたクラスやメソッド、及びそれらに依存する(Systemアセンブリなどの)クラスやメソッドを用いないようにコードを記述します。 ある程度強い制約なのでアプリの最小サイズを極めたい人向けです。

micro mscorlibで使用可能なクラスとメソッドは ここ で確認できます(最新のマニュアルには存在しないみたいです)。 micro mscorlibはビルド時にクラスとメソッドを除くわけではなく、link.xmlによる抑制は効かないため注意が必要です。

ここでひとつ疑問なのが、Strip assembliesで使用しないクラスやメソッドは除かれるにもかかわらず、 micro mscorlibを使う意味があるのかということです。 これを確かめるには実際に比較するのがイチバン、ということでStrip ByteCodeの出力とUse micro mscorlibの出力を比較したところ、 ちゃんとmicro mscorlibの方がサイズが小さくなっていました。 静的解析では判断しきれない部分までmicro mscorlibでは除かれているのかもしれません。

ちなみにStrip ByteCodeとUse micro mscorlibのサイズ差は、 メタデータで約150KB、アセンブラのソースコードで約3000KB(3MB)でした。 アセンブラのソースコードはこれから機械語に変換される(テキスト→バイナリ)ため実際にはこの差は大幅に小さくなります(メタデータは多分そのまま)。 またさらにAppStore提出時に暗号化(サイズ増加)と圧縮(サイズ減少)がかかるそうなので最終的なサイズ差はよくわかりません。 ただ相当ぎりぎりまでリソースを詰め込みたい場合以外では気にするほどの大きさにはならないと思います。

まとめ

オプション 判断基準
Disabled 開発時・リフレクションを多用
Strip assemblies 特に必要なし?
Strip ByteCode 通常
Use micro mscorlib 究極的なサイズ削減

プラットフォームによる差異

Stripping Levelの設定は各プラットフォームで共有されますが、 プラットフォームによって必ずしも設定通りには動かないようなのでまとめておきます。

iOS

iOSはすべてのオプションに対応しています。 というのも、Strip ByteCodeはiOSでの動作を前提にしたものであるためです。

.NET Frameworkというのは通常、C#やUnityScriptといった人間の理解しやすい言語から機械語に比較的近い中間言語へと変換し、 その後実行環境で中間言語を機械語に変換しながら実行します。 実行時に機械語へと変換(コンパイル)するためこれをJIT(Just-In-Time)コンパイルといいます。 これにより、ひとつのコードが様々な機械の上で実行できるようになります。

しかし、この方法はiOSでは使用できません。 iOSではセキュリティのために実行時のコード生成が禁止されているためです。

そこで、Unityが内部的に用いているMonoでは事前に中間言語を機械語へと変換してしまう手法をとっています。 JITコンパイルに対してこれをAOT(Ahead-Of-Time)コンパイルといいます。 この手法では中間言語と機械語の両方のコードが生成されるためこれを除去するためにStrip ByteCodeが提供されています。

Android

AndroidはStrip ByteCodeには対応していません(マニュアルより)。 またどうやらUse micro mscorlibにも対応していないようです(ビルド結果より)。

Androidでは実行時のコード生成は特に禁止されていません。 そのためMonoではJITコンパイルによって中間言語を実行しているようです。

そのため機械語コードは実行時にしか作成されず、中間言語のコードを除去してしまうとゲームコードそのものが消失してしまいます。 結果としてStrip ByteCodeを行うことができないためこのオプションには対応していません。 Use micro mscorlibには対応してもよいように思うのですが、ビルド結果のアセンブリを確認したところ通常のmscorlibが使用されているようでした。

IL2CPP (Scripting Backend)

さて、ここまで長々と説明してきたStripping Levelについてですが、近い将来消滅するかもしれません。 というのもIL2CPPという技術をUnityが推し進めており、多くのプラットフォームにおいてビルドの方法が変わるためです。

IL2CPPというのはその名の通り中間言語をC++のソースコードへと変換する技術になります (Intermediate Language to C Plus Plus *3 )。 AOTで中間言語をC++のソースコードへと変換し、さらにプラットフォーム固有のコンパイラで機械語へと変換します。 実行時にはIL2CPP用のVM(Virtual Machine)を利用して機械語コードを実行します。

正直、現iOSのコンパイル方法との違いがそれほどよくわかっていないのですが、 一度C++のソースコードに変換することでプラットフォームごとのコードの最適化などができ、 実行速度などが向上するらしいです。 WebGLやAndroidなど順次このビルド方法へと切り替えていく方針のようです。

現時点でiOS、AndroidともにScripting Backendの項目を"IL2CPP"に変更するとIL2CPPに切り替わります。 そして、その場合にはStripping Levelの項目がStrip Engine Codeというチェックボックスの項目に変わります。 この項目をチェックするとStrip ByteCode相当の処理が、チェックしないとDisabled相当の処理が行われます。 おそらくあとの二つのオプションはあまり必要ないと判断されて削除されたのでしょう。

Strip Engine Code 判断基準
Off 開発時・リフレクションを多用
On 通常

おわりに

総まとめとしては

  • IL2CPP使おう
  • リフレクションやめよう
  • Strip Engine CodeをOnにしよう

の三点です。

結構気合い入れて調べた割には結論が、IL2CPPになるからもう使わないんじゃない?という悲しい結果に・・・
まあ、変なことに悩まなくて済むのはユーザとしては非常に嬉しいのですが。

付録:Unity関連ファイルの構成

今回いろいろとUnity関連のフォルダを漁ったのでその構成についてメモしておきます。

  • Unity関連DLL
    • (Unityのインストールディレクトリ)\Editor\Data\Managed
    • UnityEngine.dllやUnityEditor.dllなど
  • Mono関連DLL(実行時)
    • (Unityのインストールディレクトリ)\Editor\Data\Mono\lib\mono\(2.0 | unity | unity_web | micro | bare-minimum)
    • mscorlib.dllやSystem.dllなど
    • Boo.Lang.dllやUnityScript.Lang.dllなど
  • Mono関連DLL(開発時[VSTU])
    • C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Unity (Full | Subset | Web | Micro) v3.5
    • メタデータのみのmscorlib.dllやSystem.dllなど
    • メタデータのみのBoo.Lang.dllやUnityScript.Lang.dllなど
  • Unity拡張機能関連DLL
    • (Unityのインストールディレクトリ)\Editor\Data\UnityExtensions\Unity\(GUISystem | Networking | Advertisements | ...)
    • UnityEngine.UI.dllやUnityEngine.Networking.dllなど
  • 各プラットフォームのビルド関係ファイル

    • (Unityのインストールディレクトリ)\Editor\Data\PlaybackEngines\(iOSSupport | AndroidPlayer | ...)
    • ビルド後のプロジェクトに含まれるファイルは大体ここ
  • iOSのプロジェクト構成

    • Managed DLL
      • (プロジェクトのルートディレクトリ)\Data\Managed
      • Assembly-CSharp.dllがユーザ記述のC#のコンパイル結果
    • アセンブラソース
      • (プロジェクトのルートディレクトリ)\Libraries
      • (元のDLLの名前).s
  • Androidのプロジェクト構成

    • Managed DLL
      • (プロジェクトのルートディレクトリ)\assets\bin\Data\Managed
      • Assembly-CSharp.dllがユーザ記述のC#のコンパイル結果



執筆時のUnityのバージョン:5.3.2f1

*1:さらに将来的にはなくなっている可能性も否めない

*2:コンパイラも参照先が存在しないことを検出できないため

*3:コンピュータの世界で2->toや4->forの変換はよく使われます