找回密码
 立即注册
查看: 455|回复: 3

[笔记] 【Unity】引擎编译时间优化

[复制链接]
发表于 2023-2-9 09:01 | 显示全部楼层 |阅读模式
前言

项目的编译启动时间越来越长,逐渐无法忍受,单次启动时间高达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 files
1Assembly-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.25s
InitializeOnLoad 加宏处理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. 序列化所有公共字段和私有字段值,标有 [NonSerialized] 的值除外。
  • 托管的包装器与原生 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.对于使用 [ExecuteInEditMode] 属性的脚本: 1. 调用 OnEnable()。 2. 调用 OnDisable()。 3. 调用 OnDestroy()。
  • 调用包含 InitializeOnLoadInitializeOnLoadMethod 的方法。
  • 调用 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的相关实验

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-2-9 09:03 | 显示全部楼层
1
发表于 2023-2-9 09:10 | 显示全部楼层
直接暴躁地把enter play mode底下的reload全取消掉[捂脸]
发表于 2023-2-9 09:19 | 显示全部楼层
为什么给7950x,32线程5.6主频的我推这种文章[doge][doge][doge]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 10:54 , Processed in 0.093911 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表