有史以来最快的Unity编辑器版本,Unity 2020.2 中的性能优化

2020/11/3 15:33:15

编写高性能代码是高效软件开发不可或缺的一部分,也一直是Unity遵循的开发准则。在两年前,我们大胆地组成了一支专门的Optimization Team优化团队,将性能作为单独的类别进行优化。


Unity 2020.2有几项全新的优化功能已在beta中开放测试,本文将介绍2020.2中的新进展以及团队的努力,完整的改进请查看Unity 2020.2 beta发布说明。



嵌套预制件优化


我们优化团队与功能的开发方——Scene Management Team场景管理团队紧密合作,优化了嵌套预制件的多个方面,包括:


 减少了动态属性组的修改必要

 更改了修改组分类规则

 改为使用哈希名称集,方便查找


在加载预制件实例时,实例上与源预制件资源不同的属性会被修改,此类操作被称为属性修改PropertyModifications)。在合并属性修改时,系统会更新一个动态属性组,插入必要的修改,而属性组的结构过于庞大,导致整个流程非常消耗性能。如果保留新属性组的属性修改,跟踪有更新的属性,则流程速度可以加快最多60倍(在测试项目中从3300毫秒降到了54毫秒)。


在更新属性修改时,由于修改列表可能无法被正确分类,因此需要一个新的分类。此前,新分类的建立是将旧修改分成新的修改组,在测试时,该方法花费了11秒。然而如果在容器内完成分类、指定到修改上,则分类流程可降低至44毫秒(快了250倍),如果没有修改,则为11毫秒(快了800倍)。


此外,若将容器改为一个哈希集合,则在修改列表中搜索propetyPath的速度可快上50倍(从300毫秒降低至6毫秒)。而属性对比文件的生成也能得到优化。



脚本导入器Scripted Importer)优化


  优化了Register Scripted Importers中线性搜索的嵌套循环。


数据库拓展性测试显示,编辑器的脚本导入器(Scripted Importers)注册功能在需注册的导入器数量变多时性能会降低。现在功能会按照拓展名将导入器储存为一个词典,加快对导入冲突的搜寻。在处理100到5000个处理器时,总体速度的优化效果约在12到800多倍之间(请在下方图表中查看整体效果改善):




编辑器工作流优化


  减少了编辑器关键任务中的字符串复制与内存分配操作

  使用临时内存优化场景引用的查找


在处理只存在于单帧上的字符串时,我们团队使用临时内存标签替换了那些执行缓慢的内存分配。Unity中的大部分字符串都仅作为单次函数调用的本地变量存在,因此可以快速使用内存分配器。现在大部分字符串通用函数都会使用临时内存。


该优化的一部分其实已经于2020.1落地。下方图表显示出2020.1.0a12到2020.2.0a20之间的多次迭代中(x轴为迭代数,y轴为字符串分配次数)有多少慢字符串内存分配的操作已被移除。



另一项编辑器工作流优化在Find References In Scene中。此前,在大型场景的Project视窗中右击资源、选择“查找场景引用处Find Refernces in Scene)”,查找过程会非常慢。而我们通过减少多余的智能指针间接引用、利用临时缓存,将整个过程的速度提高了约10%


当场景的引用缺失时,每次间接引用智能指针都会让系统尝试在文件系统中加载无效的文件名。通过检测出无效文件名、阻止文件系统执行失败的文件访问操作,整个执行时间可减少至多3倍。



Job System


  在安排执行大型并行Job时,优化后的JobQueue运行速度为原先的2倍


我们与其他内部团队一起合作,优化了Job System的JobQueue。优化始于今年早些时候对DOTS示例项目的分析,在分析后,我们发现AtomicStack::Pop()的耗能意外的高。进一步调查发现问题出在JobQueue的内存管理系统中,其中JobInfo的问题最为突出,它使用了AtomicStack作为条目的内存管理池。


在面向数据技术栈(DOTS)中,部分ForEach在内存分配时每个元素都要使用一个Popo(),在接触分配时要使用一个Push()。这会导致AtomicStack头部条目出现冲突。


Unity的另一支团队制作出了一个全新的Atomic容器,支持在单次操作中分配一堆的元素,防止Pop()出现在每个ForEach中。


早期的本地性能测试表明方法很有成效,随着Job工作线程的增加,系统运行规模至多高了2倍



DOTS团队的一位成员指出新的容器在JobQueue For Each中的性能收益会很大。


本例运行在安卓设备上。绿色为新代码,红色为旧代码:




Camera.main优化


  将主摄像机节点储存在单独的列表中,移除了不必要的搜索操作

由于Camera.main的搜索功能并不如人意,我们通常不建议使用该函数。此前,所有带有标签的GameObject都会倍搜索一遍,其中标签匹配的GameObject会被储存到一个临时数组中。接着系统会搜索这个列表,如果有启用了摄像机组件的对象,则返回该对象。

新方法则会将带有MainCamera标签的对象储存在一个专门的列表内,并不会在第二个列表中搜寻匹配对象,而是直接查询,搜寻到匹配对象后返回。所有被搜寻对象都带有MainCamera标签,因此搜索成功的机率也就更高。

在包含50000个对象的测试中,我们发现搜索的速度增加了21000到51000倍。



在Spotlight Team的一个客户项目中,原先许多几百毫秒的函数运行时间都消失了。




RenderManager摄像机使用优化


  减少摄像机分类操作在RenderManager中的影响


此前,每从RenderManager类中添加/移除一个摄像机,系统会更新一个相连的列表,保持活跃摄像机按深度算出的排序。每次更改系统都需要执行一次内存分配和指针间接引用,来检测每个摄像机的深度,在有多个摄像机时,此类操作运行会很缓慢。


如今,该列表仅会在需要排序时进行分类,因为只有渲染需要使用分类列表。在加载时,摄像机会在一个平铺数组内被添加/移除(所需内存分配更少),而分类仅会在系统需要列表时(即渲染时)被执行。下方测试表明了改善后的最终耗时(最右边的橘黄色条为新代码):




纹理加载优化


  2D纹理与Cubemap的创建可在大部分图形后端的线程上完成

  优化了主机2D纹理与Single Mip Cubemap的加载


为了减少纹理加载时的故障,我们将2D纹理的创建从图形线程转移到了工作线程上。Unity 2019版本已在大部分图形后端上应用了该优化。而在Unity 2020.2中,我们添加了对DirectX 12的支持,解决了8k纹理加载时的一个80毫秒的停滞故障。



我们还优化了主机上的Texture 2D加载,禁用了纹理swizzling(一种记录图像像素矢量、提高渲染速度与效率的方法),直接将纹理加载到GPU内存中。如今,不同大小和平台的2D纹理在加载时性能提高了至多30%


主机的Cubemap加载也经过了优化。部分主机在加载2k分辨率的Cubemap时,任务线程的耗时减少了30毫秒,单个纹理的加载总时间减少了15毫秒。



Profile Analyzer 1.0.0


  Profiler Analyzer在2020.2中进入1.0.x周期,成为经验证资源包


代码剖析与分析是我们优化性能的指南针,我们会将具体平台的分析工具与Unity自己的Profiler组合使用,同时还编写了一个Profile Analyzer工具(点击回看)来辅助代码分析。工具在2020.2中将作为经验证资源包发布。


Profile Analyzer 1.0.0及其更新包括了一系列改善使用体验的Bug修复、一些性能优化,添加了一些小功能,如:


 一个带有标记、可查看线程的可开关分析列

 帧时间UI图表的多选支持

 线程选择UI中的分类功能

 Profiler团队将负责未来工具的开发



快加入Unity 2020.2 beta内测吧


友情提示:在试用beta之前,我们建议先备份项目,此建议适用于所有与beta和预览包相关的试用。Beta项目并不能用于实际生产,而是为了让用户评估新功能、提供反馈的。对于那些在实际制作中的项目,我们推荐使用经验证的资源包和最新的Unity 2019 LTS。


我们期待着大家的反馈。



回到顶部