【Unity】「AssemblyDefinitionを分けるとコンパイルが高速になる」ってホント?検証してみた!
あけましておめでとうございます。バーチャルキャスト開発部のnotargs(のたぐす)です。
Unity 2017.3
から、Assemblyを分割するためのAssemblyDefinition
という機能が追加されました。
この機能を使うと、次のような恩恵が受けられます。
- 更新がないAssemblyはコンパイルされないため、コンパイル時間の短縮に繋がる
- アセンブリ同士の依存関係を絞り、アーキテクチャを明確にできる
- internal(同一Assembly内でのみアクセスできるアクセス修飾子)が使える
さて、この機能ですが、コンパイル時間の短縮
があまり体感できなかったので、実際にどの程度短縮されているかを調べてみました!
使用したUnityのバージョン
今回の検証にはUnity 2018.4.14f1
を使用しています。
コンパイル時間を計測する
コンパイル時間を調べるためにエディタ拡張を作りました。
Assemblyのコンパイル開始/終了を通知するデリゲートCompilationPipeline.assemblyCompilationStarted
とCompilationPipeline.assemblyCompilationFinished
を使い、アセンブリの更新にかかった時間を計測しています。
ついでにトータルのコンパイル時間とアセンブリのリロードにかかった時間を表示する仕組みも用意しました。
コンパイルの待ち時間に行われていることを可視化できるほか、不要なAssembly同士の依存関係も見つけられるので、開発中も画面端に常に表示しておくと捗ります。
コード全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
using System; using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; namespace Editor { public sealed class CompilationLogger : EditorWindow { [SerializeField] private bool _isTrackingTime; [SerializeField] private double _compileStartTime; [SerializeField] private double _assemblyReloadStartTime; [SerializeField] private Vector2 _scroll; [SerializeField] private List<string> _logs = new List<string>(); [SerializeField] private List<AssemblyCompileTimeStamp> _timeStamps = new List<AssemblyCompileTimeStamp>(); private readonly StringBuilder _stringBuilder = new StringBuilder(); [MenuItem("Tools/CompilationLogger")] private static void Open() { GetWindow<CompilationLogger>(); } private void OnEnable() { CompilationPipeline.assemblyCompilationStarted += OnAssemblyCompilationStarted; CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished; AssemblyReloadEvents.beforeAssemblyReload += BeforeAssemblyReload; AssemblyReloadEvents.afterAssemblyReload += AfterAssemblyReload; EditorApplication.update += Update; titleContent = new GUIContent("CompilationLogger"); } private void OnDisable() { CompilationPipeline.assemblyCompilationStarted -= OnAssemblyCompilationStarted; CompilationPipeline.assemblyCompilationFinished -= OnAssemblyCompilationFinished; AssemblyReloadEvents.beforeAssemblyReload -= BeforeAssemblyReload; AssemblyReloadEvents.afterAssemblyReload -= AfterAssemblyReload; EditorApplication.update -= Update; } private void OnAssemblyCompilationStarted(string assemblyName) { Log($"- Assembly compile start:\t\t{assemblyName}"); _timeStamps.Add(new AssemblyCompileTimeStamp(assemblyName, EditorApplication.timeSinceStartup)); Repaint(); } private void OnAssemblyCompilationFinished(string assemblyName, CompilerMessage[] messages) { var time = double.NaN; for (var i = 0; i < _timeStamps.Count; i++) { var timeStamp = _timeStamps[i]; if (timeStamp.AssemblyName == assemblyName) { time = EditorApplication.timeSinceStartup - timeStamp.Time; _timeStamps.RemoveAt(i); } } Log($"- Assembly compile end:\t[{time:0.000}s]\t{assemblyName}"); Repaint(); } private void BeforeAssemblyReload() { _assemblyReloadStartTime = EditorApplication.timeSinceStartup; Log($"- Start assembly reload"); Repaint(); } private void AfterAssemblyReload() { var reloadTime = EditorApplication.timeSinceStartup - _assemblyReloadStartTime; Log($"- End assembly reload\t[{reloadTime:0.000}s]"); Repaint(); } private void OnGUI() { EditorGUILayout.BeginVertical(); _scroll = EditorGUILayout.BeginScrollView(_scroll); _stringBuilder.Clear(); foreach (var log in _logs) { _stringBuilder.AppendLine(log); } EditorGUILayout.TextArea(_stringBuilder.ToString(), GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void Update() { if (EditorApplication.isCompiling && !_isTrackingTime) { _compileStartTime = EditorApplication.timeSinceStartup; _isTrackingTime = true; _logs.Clear(); Log("** Compile start **"); } else if (!EditorApplication.isCompiling && _isTrackingTime) { _isTrackingTime = false; var compileTime = EditorApplication.timeSinceStartup - _compileStartTime; Log($"** Compile end **\t[{compileTime:0.000}s]"); } Repaint(); } private void Log(string message) { _logs.Add(message); _scroll.y = float.MaxValue; } [Serializable] private readonly struct AssemblyCompileTimeStamp { public string AssemblyName { get; } public double Time { get; } public AssemblyCompileTimeStamp(string assemblyName, double time) { AssemblyName = assemblyName; Time = time; } } } } |
検証に利用するプロジェクト
Assembly1
、Assembly2
、Assembly3
の3つのAssemblyDefinitionを切ったプロジェクトを用意しました。
Auto Referenced
はOnのままで、Assembly3
のAssembly Definition References
にAssembly1
とAssembly2
の両方を追加しています。
図で書くと次のような依存関係となっています。
いろいろ試してみる
AssemblyDefinitionAssetの設定を変えてみる
試しにAssembly1
のAllow 'unsafe' Code
にチェックを入れて保存してみると、このようなログが出力されました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
** Compile start ** - Assembly compile end: [1.108s] Library/ScriptAssemblies/Unity.Analytics.DataPrivacy.dll - Assembly compile end: [1.164s] Library/ScriptAssemblies/Assembly1.dll - Assembly compile end: [1.211s] Library/ScriptAssemblies/Assembly2.dll - Assembly compile end: [1.191s] Library/ScriptAssemblies/Unity.PackageManagerUI.Editor.dll - Assembly compile end: [1.172s] Library/ScriptAssemblies/Unity.CollabProxy.Editor.dll - Assembly compile start: Library/ScriptAssemblies/Assembly3.dll - Assembly compile end: [1.437s] Library/ScriptAssemblies/Unity.TextMeshPro.dll - Assembly compile start: Library/ScriptAssemblies/Unity.TextMeshPro.Editor.dll - Assembly compile end: [0.298s] Library/ScriptAssemblies/Assembly3.dll - Assembly compile end: [0.477s] Library/ScriptAssemblies/Unity.TextMeshPro.Editor.dll - Assembly compile start: Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Assembly compile end: [0.385s] Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Start assembly reload - End assembly reload [1.478s] ** Compile end ** [3.785s] |
同様に、新しいAssemblyDefinitionが追加/削除されたときも、Packageを含めた全てのAssemblyがコンパイルされます。
Assembly1を変更してみる
Assembly1
にあるクラスを変更してみると、次のログが出力されました。
1 2 3 4 5 6 7 8 9 10 |
** Compile start ** - Assembly compile start: Library/ScriptAssemblies/Assembly1.dll - Assembly compile end: [0.446s] Library/ScriptAssemblies/Assembly1.dll - Assembly compile start: Library/ScriptAssemblies/Assembly3.dll - Assembly compile end: [0.315s] Library/ScriptAssemblies/Assembly3.dll - Assembly compile start: Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Assembly compile end: [0.314s] Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Start assembly reload - End assembly reload [1.482s] ** Compile end ** [2.979s] |
Assembly1
と、それに依存したAssemblyがコンパイルされていることがわかります。
Assembly1
と依存関係のないAssembly2
はコンパイルされていません。
Assembly3を変更してみる
Assembly3
にあるクラスを変更してみると、次のログが出力されました。
1 2 3 4 5 6 7 8 9 10 |
** Compile start ** - Assembly compile start: Library/ScriptAssemblies/Assembly1.dll - Assembly compile end: [0.446s] Library/ScriptAssemblies/Assembly1.dll - Assembly compile start: Library/ScriptAssemblies/Assembly3.dll - Assembly compile end: [0.315s] Library/ScriptAssemblies/Assembly3.dll - Assembly compile start: Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Assembly compile end: [0.314s] Library/ScriptAssemblies/Assembly-CSharp-Editor.dll - Start assembly reload - End assembly reload [1.482s] ** Compile end ** [2.979s] |
Assembly3
の依存先であるAssembly1
とAssembly2
はコンパイルされていません。
Auto Referencedを外してみる
Auto Rerenced
はAssemblyをAsssembly-CSharp
/Assembly-CSharp-Editor
に追加するオプションですが、不要なことも多いと思います。
全てのAssemblyのAuto Referenced
のチェックを外し、Assembly1
のクラスを変更してみました。
次のようなログが出力されます。
1 2 3 4 5 6 7 8 |
** Compile start ** - Assembly compile start: Library/ScriptAssemblies/Assembly1.dll - Assembly compile end: [0.454s] Library/ScriptAssemblies/Assembly1.dll - Assembly compile start: Library/ScriptAssemblies/Assembly3.dll - Assembly compile end: [0.317s] Library/ScriptAssemblies/Assembly3.dll - Start assembly reload - End assembly reload [1.482s] ** Compile end ** [2.648s] |
Assembly-CSharp
、Assembly-CSharp-Editor
との依存関係がなくなったため、Assembly1
とAssembly3
のみがコンパイルされていることがわかります。
まとめ
いかがでしたか?
以上の検証から、AssemblyDefinitionを適切に分ければコンパイルは高速化される
ことがわかりました。
また、コンパイル時間を減らすためには、次の点に気をつければいいことがわかります。
- 頻繁に変更が発生するAssemblyはできる限り依存元のAssemblyを減らす
- 不要なら
Auto Referenced
のチェックを外す
バーチャルキャストのクライアントでは現状191個のAssmblyがあり、フルコンパイルだと28秒ほどかかってしまいますが、このあたりを意識して設定した結果、局所的な変更であれば8秒程度で終わるようになりました。
コンパイル時間を大きく削減できるので、ぜひ意識してみましょう!
【おまけ】その他検証中に気づいたこと
Riderの設定を見直して速度を更に上げる
IDEにRiderを使っている場合、SettingsのLanguage & Features/Unity Engine
内にAutomatically refresh assets in Unity
という設定項目があります。
こちらはファイルが更新されたとき、自動的にバックグラウンドでUnityのrefresh assetsを行ってくれるオプションですが、オフにすることで、コンパイル前に走るrefresh assetsの待ち時間を大幅に減らせました。
refresh assetsが長いと感じる人はここの設定もあわせて見直してみましょう。
Project内のAssemblyDefinitionを全て検索する
ProjectViewにt:AssemblyDefinitionAsset
と入力すると、プロジェクト内の全てのAssemblyDefinitionを検索できます。
Auto Referenced
を一括でOffにする時などに便利なので、ぜひ活用してみてください。
- Tag
- Unity