【Unity】引擎编译时间优化
前言项目的编译启动时间越来越长,逐渐无法忍受,单次启动时间高达2分半钟,调试功能的时间被大批量的浪费。
优化冷启动和编译时间越来越迫切。
插件:compilation-visualizer 可以清楚的看到编译的整个过程
插件:Editor Iteration Profiler 可以用来定位重载和编译耗时的工具一、CPU线程数
Demo工程
自己测试项目在4线程i5-3317U 1.70GHz,10年前笔记本的编译时间:39s
在公司16线程,i7-11700 2.5GHz的台式电脑上的编译时间:9s
所以,首先,钱能解决的问题,加CPU线程数!能解决并行编译的最大数量,对于大量dll项目,可以加快编译时间。
接下来探讨没钱的解决办法。
<hr/>二、项目问题分析
2.1 全量编译
目前项目全量编译情况,编译总时长:116.65s,其中编译76.48s,重载37.20s。编译的程序集有112个
(iterations有2个,是迭代了2次,Burst的editor dll导致项目重现编译重载了一下)
2.2 增量编译
单脚本修改,编译耗时:总时长41.36s 编译耗时:18.13s,重载耗时22.86s
2.3 问题罗列
[*]项目插件太多,导致需要编译的dll数目很大
[*]部分dll编译时间耗时过长(Assembly-CSharp 需要拆分)
[*]Assembly-CSharp.dll 重复编译2次(Burst编译导致项目 编译+reload 2次,TextureCombiner 依赖Burst,导致编译顺序不大合理)
TextureCombiner的编译顺序不大合理,应该是引用依赖导致
Assembly-CSharp.dll 重复编译
项目插件太多,导致需要编译的dll数目很大
部分dll编译时间耗时过长
三、原理分析
在解决问题之前,先搞清楚,Unity引擎转圈圈操作了什么?
[*]编译(Compilation)
[*]重载(Reload)
3.1 编译
编译部分主要是对项目内的脚本代码按程序集划分,编译成一个一个的dll,然后Unity预定义的4个程序集依赖这些dll,进行编译,得到引擎用的4个dll。(个人理解)
那么什么是程序集呢?
3.1.0 程序集是什么?
官方解释:程序集一个C#代码库,包含编译后的类和结构体,并定义了对其他程序集的引用,表现为dll或exe文件。
程序集类似一个文件夹,可以对其中的脚本进行管理。Unity有4个预定义程序集,编译的顺序如下:Unity API 文档对编译流程的说明
PhaseAssembly nameScript files1Assembly-CSharp-firstpass名为 Standard Assets、Pro Standard Assets 和 Plugins 的文件夹中的运行时脚本。2Assembly-CSharp-Editor-firstpass名为 Editor 的文件夹(位于名为 Standard Assets、Pro Standard Assets 和 Plugins 的顶级文件夹中的任意位置)中的 Editor 脚本。3Assembly-CSharp不在名为 Editor 的文件夹中的所有其他脚本。4Assembly-CSharp-Editor其余所有脚本(位于名为 Editor 的文件夹中的脚本默认情况下,游戏的脚本会编译进 Assembly-CSharp 这个程序集中,每次修改脚本都会重新编译这个程序集,随着项目脚本增多,编译时间会逐渐增加,而且任何脚本都可以直接访问其他脚本,这也会使代码重构变得困难。
3.1.1 编译优化手段1:移动到 Standard Assets
所以在优化增量编译的情况下,最快的方式:
[*]把插件、编辑器代码、项目框架等不常变动的代码移动目录到 Standard Assets 下
这样,这些不常改动的代码就会从 Assembly-CSharp 这个程序集移动到 Assembly-CSharp-firstpass 这个程序集里,这些代码只会在项目启动的时候编译一次,后续在业务开发的时候,没有改动到这些代码,也就不会编译这些代码,大大减少重复编译的时间。
优化编译时间效果比对(编译重载时间受缓存等环境影响,取的只是单次值,没有取平均值)
总耗时总提升比例编译编译提升比例重载重载优化比例未处理41.36s18.13s22.86s移动到 Standard Assets34.43s快6.93s
提升16.7%15.07s快3.06s
提升16.9%19.25s快3.61s
提升15.7%
3.1.2 编译优化手段2:Assembly definitions
接着就是看一下 Assembly-CSharp.dll 的编译时间,如果 Assembly-CSharp 的编译时间还是很长,可以考虑拆分 Assembly-CSharp 这个程序集,Unity提供了 Assembly definitions 让用户自己定义程序集,起到拆分模块左右,减少耦合。
程序集定义 (Assembly Definition) 属性 - Unity 手册
[*]Assembly definitions 使用方法:在文件夹内右键 -> Create - > Assembly Definition。
[*]该文件夹下所有脚本都归这个程序集管理。
编译完成之后,就会在 项目名\Library\ScriptAssemblies 文件夹下生成对应名字的程序集。
项目目录下直接生成解决方案
项目名\Library\ScriptAssemblies 文件下编译生成dll
熟悉了 Assembly definitions 的使用,就能根据业务模块进行拆分 Assembly-CSharp 程序集了,拆分了不同程序集,就能最大利用线程优势进行多线程编译,加快编译时间了。
优化编译时间效果比对
项目依赖太复杂了,根本拆不动!+ 没时间拆3.1.3 编译优化手段3:删除无用插件
[*]打开 Package Manager,去除无用插件和无用包,减少dll数量
[*]通过 compilation-visualizer,逐个研究dll使用有用,没用删除
优化编译时间效果比对
<hr/>3.2 重载 Assembly Reload
上面讲的是Unity引擎的编译部分,还有很重要的一部分是重载部分。Unity手册:关于进入运行模式的详细信息
当编译完代码之后,Unity需要进行一次Assembly Reload,将新编译的所有类替换入CLR。
由于替换过程中类中的数据是无法保留的,因此需要将所有暂存的对象都序列化一次,在替换后再反序列化回去,这个过程占用了大量的时间。如果项目内有大量的序列化内容,这个需要消耗的时间就会线性增加。
我找到了一个可以用来定位重载和编译耗时的工具:Editor Iteration Profiler
使用工具可以清楚的看到,编译和耗时具体消耗点在哪里,然后就可以逐个击破,跟性能优化一个道理。
空工程的域重载流程
上图可以看到处理 InitializeOnLoad 属性花掉了8.3s,说明项目里面滥用了这个属性,导致重载耗时偏高。
3.2.1 重载优化手段1
通过给 InitializeOnLoad 和 InitializeOnLoadMethod 加宏,来控制一些工具类的开关,业务开发环境下,可以对这些工具类的初始化进行关闭,来优化编辑器重载速度
优化编译时间效果比对(编译重载时间受缓存等环境影响,取的只是单次值,没有取平均值)
总耗时总优化比例编译编译优化比例重载重载优化比例未处理移动到 Standard Assets34.43s15.02s19.25sInitializeOnLoad 加宏处理28.19s快6.24s
提升18%15.96s慢0.94s
降低6%12.13s快7.12s
提升36%!
杂优化点:
[*]关闭未使用的选项卡。(序列化耗时)
[*]关闭burst的编译,命令行添加参数--burst-disable-compilation【无效,还是会编译2次】
[*]在最新的 Unity 2020.1 中有一个新选项: Preferences > General > Directory Monitoring。(2021 Preferences > Asset Pipeline)选中此框将导致 Unity 使用底层操作系统 API 监视新更改,而不是扫描整个 Assets 文件夹以查找更新文件。使用此功能,资产扫描时间应降至几乎为零。
[*]每次编译之后都要 reload domain,而且进入播放前也会reload domain(可以优化成手动重载):Unity 手动编译 Reload脚本 减少等待时间
[*]OnPostProcessAllAssets
[*]Unity 重新生成TypeCache。这需要大约 300 毫秒,具体取决于程序集中的类型数量。(但是,从长远来看,使用此类可以节省时间)
[*]用于 Rider 和 Visual Studio 等编辑器的包通常需要一些时间来重新生成解决方案文件。(6-7s,现在项目)
总结
编译优化
[*]移动到 Standard Assets
[*]利用 Assembly definitions 组织代码引用关系,拆分dll
重载优化
[*]InitializeOnLoad 加宏处理,按需开启
附录
这个感觉有必要摘录过来!!
关于进入运行模式的详细信息
启用场景重新加载和域重新加载后,以下是 Unity 进入运行模式时执行的所有进程和事件的完整列表:
[*]引发 AssemblyReloadEvent beforeAssemblyReload 事件。
[*]停止 C# 域: a. 针对所有 ScriptableObject 和 MonoBehaviour 调用 OnDisable()。 b. Unity 等待所有异步操作完成。
[*]序列化所有 MonoBehaviour 和 ScriptableObject 的状态。 a. 调用 OnBeforeSerialize()。 b. 序列化所有公共字段和私有字段值,标有 的值除外。
[*]托管的包装器与原生 Unity 对象断开连接。
[*]重新加载 Unity 子域: a. 卸载 Mono 域: i. 引发 AppDomain.DomainUnload 事件。 ii.销毁 Unity 子域
1. 调用 GC 和终结器。 2. 终止线程。 3. 删除所有 JIT 信息。 b. 创建新的 Unity 子域。
[*]加载程序集: a. 加载系统程序集。 b. 加载 Unity 程序集。 c. 加载用户程序集。
[*]初始化同步上文。
[*]恢复脚本状态。 a. 重新创建所有 Unity 对象的可编程部分。 i. 调用构造函数,并为统计信息分配默认值。 b. 反序列化所有 Unity 对象的状态: i. 恢复所有 Unity 对象的序列化状态。 1. 引发 OnAfterDeserialize 事件。 ii.调用 OnValidate()。 iii.对于使用 属性的脚本: 1. 调用 OnEnable()。 2. 调用 OnDisable()。 3. 调用 OnDestroy()。
[*]调用包含 InitializeOnLoad 和 InitializeOnLoadMethod 的方法。
[*]调用 AssemblyReloadEvent afterAssemblyReload。
<hr/>todo 持续优化ing~
参考
这篇写得特别好:Fast Domain Reloads in Unity — John Austin
update-regarding-increased-script-assembly-reload-time :
使用环境变量 UNITY_DIAG_ENABLE_DOMAIN_RELOAD_TIMINGS 启用 域重新加载profile,之后,在您的编辑器日志中(%LOCALAPPDATA%\Unity\Editor\Editor.log),您将看到域重新加载的详细时间。优化编译速度 选项:Editor Iteration Profiler的使用
Configurable Enter Play Mode
Unity 定义程序集Assembly definitions
【躬行】-Assembly definitions的相关实验 1 直接暴躁地把enter play mode底下的reload全取消掉[捂脸] 为什么给7950x,32线程5.6主频的我推这种文章
页:
[1]