【译】让UI在后台运行的更快

发表于2016-02-06
评论4 2.7k浏览

原文地址:http://blogs.unity3d.com/2015/09/07/making-the-ui-backend-faster/

原文作者未做版权声明,视为共享知识产权进入公共领域,自动获得授权


  在Unity 4.6 / 5.0的时候,UI系统的渲染批次生成非常慢。这里面有一些原因,但最后发布版本的最后期限使得我们没有继续关注这一块,而是集中精力在UI的使用以及API部分。在完成UI部分的冲刺中,我们很幸运的得到了一些优化方面的帮助。在版本发布之后,我们决定回头分析下到底为什么这么慢以及该如何修复它们。如果你只是想大概了解了解我们是怎么做的:我们把UI所有的工作(除了工作调度意外)都从主线程中拿出去了,并且我们还修正了一些批次排序的Bug。


性能工程

  我们开发了一些UI性能测试场景来作为基线评判性能上的改变。它们采用了一些方法给UI施加压力。用来测试批次排序和批次生成最合适的场景是一个完全被按钮填满的画布,所以在决定哪些该合并的时候总有一些开销。这个测试场景还修改UI元素使得需要每帧重新合并批次。UI的排序顺序在测试中是可配置的,可以是按照一个固定顺序(利用了空间紧密性)也可以是完全随机的。很明显,批次排序需要在这两种场景下都很快,但是在4.6 / 5.0 版本中它都很慢。还需要提及的一点是性能测试大概有10k左右的UI元素。这并不是一个真实场景,按照我们的经验,真实场景中每个画布大概有300个元素。所有的性能测试和调优是在我的MacBook Air上完成的(13英寸,2013年年中的版本)。

起点(4.6版本之前,没有统计数据)
  在4.6 beta版本的时候,我们得到一个反馈说当画布有很多UI元素的时候,批次排序非常慢。这主要是我们在确定哪些元素该合并为一个批次的时候用的方法很笨。我们仅仅是简单的遍历画布上的元素,看看那些元素可以合并并且赋予按照某个规则赋予它们一个深度。这意味着当我们往场景中添加一些元素的时候,我们将使用一个很慢的算法(O(N^2),从性能的角度来说这非常糟糕。

4.6 / 5.0 的release版本(基准)
  我们对批次排序做了一个优化,利用了需要排序的元素往往离得比较近这一特点。因此,我们利用空间特性构建了一些包围盒。当有新的元素加入进来的时候,是与包围盒碰撞判断该在哪而不是与一个个单独的UI元素。这对于排序元素的场景来说有一个非常显著的性能提升,但是对于UI元素随机排序的场景或者UI元素在空间上分离的很远的场景,提升就很小。如果你仔细测试过那个场景,你会看到对于随机元素的场景,批次的性能急剧下降,要大概100ms才能完成排序,这实在太慢了。
仔细看时间轴会发现还有一个地方也很糟糕,我们完全把执行流程堵住了。渲染批次的生成是在UI渲染之前、LateUpdate之后,并且经常是在场景渲染之后。看上去应该让渲染批次的生成紧挨着LateUpdate。

提高排序效率(第一次尝试)
  我们第一个关于排序效率提升的尝试还是基于位置的紧密性,但是这个方法更聪明一点。它会对整个群体标记是否可批次合并。它很快,但是在UI元素空间上比较分散的场景性能还是很差,并且当渲染元素增多的时候,它的表现并没有那么好。
没有空间聚集的输入
空间聚集的输入
这个方法不行,很明显,我们需要一个新的方法。

提高排序效率(第二次尝试)
  如前面提到的那样,对于元素分布的比较开的大UI场景,排序效率很差。我们还是针对这个问题看看有没有更好的办法。最后我们决定把画布按格子划分。每个格子对应了一个桶,任何触碰到这个格子的UI元素会被加到这个桶里。这意味着当我们加入一个新的UI元素的时候,我们只需要去和那些碰到的桶里的元素进行判断看是否需要合并批次。这对于随机排序的场景性能提升非常明显。
没有空间聚集的输入
空间聚集的输入

几何任务
  通过Unity 5引入的几何任务系统我们把UI 从主线程中拿了出去。这是一个内部功能可以把顶点Buffer放到一个单独的线程。这使得UI的很多代码无需在主线程中执行。还是会有一些小的消耗,比如管理几何任何、创建任务和任务指令。并且还消耗了一些内存,但是这和以前都在主线程中执行,效率上还是有非常显著的提高。

移除出主线程
  我们做的下一个事情是把UI创建移除出主线程。我们使用了内部的任务系统来对一系列任务进行排序。它们中的一些是串行的,另外一些没有固定的执行顺序要求、可以并发进行。下面是具体分解后的做法:
1、把进来的UI指令分解成渲染指令(1UI指令可以包含很多渲染调用,由于子网格和多个材质的缘故。)。这个任务并没有严格的执行顺序要求。它分配内存来容纳尽可能多的渲染指令。随后进来的指令被并行处理并输出。然后一个合并任务会负责对这些输出进行合并并把它们放在一个连续的内存区域。
2、对渲染指令进行排序,比较深度、覆盖范围等等。一个基本的排序是基于最小的渲染状态改变进行的。
3、渲染批次生成
· 生成渲染指令Buffer。创建渲染调用。
· 创建几何任务系统可用的位移指令。
  这个工作紧跟着LateUpdate之后。这使得他们可以在一个正常场景将要被渲染出来的时候被调用。当这些任务被安排的时候,主线程会在这些任务之前放置一个珊栏。它会等到所有数据生成以后才进行下一步。在下面的例子中,你可以看到几何任务系统一直等到渲染批次产生完毕才继续进行。
在一个比我的MacBook Air更多核的机器上执行的结果
对于一个比较复杂的UI,主线程用了0.4ms

其他我们做的提高性能的事情
2D区域切割(大部分UI不需要打开stencil buffer,这会减少draw call和状态切换)
2D区域剔除(如果你的元素在渲染范围之外,剔除它)
更聪明的画布命令系统
允许文字和普通的元素共用一个shaders/材质
最大限度地减少有关设置的pass
尽可能把UI的参数放到材质里
通常是对UI用1个设置pass,然后多个渲染调用
把UI合并到一个网格里
使用DrawIndexRange进行渲染
使用一个 VBO / index buffer
当超过2^16顶点的时候分出一个新的渲染调用

下一步
  现在渲染批次排序和生成所消耗的时间已经可以接受了。当然我们有其他的办法可以把这个过程更快一点,但是现在最大的问题在于几何任务系统所花费的时间。现在它已经从主线程中移除,并且作为一个单独的任务执行,非常适合做整理和加速。
Unity 5.2带来了很多很棒的新特性。它允许我们尽可能降低主线程中UI系统的消耗。当我们进行这项工作的时候,我们强烈依赖于分析器的指引,集中精力去解决分析器指出的热点问题。当我们发现旧的解决方案不合适的时候,我们会推到原来的方案重新解决这个问题。在Unity内部,我们做了大量这方面的工作,试图去解决你们在使用Unity过程中遇到的各种各样的问题,正是你们报告了问题,Unity才越来越好。感谢你们把你们在真实项目中遇到的问题告诉我们,这样我们才有研究解决这些问题的机会。


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引