Unity 2017 Game Optimizaiton简单翻译和总结(一):定位性能问题

英语的水平有限,在阅读时,进行了简单的记录和翻译,把一些关键的点记录下来,并加入了一些自己的理解和总结。

在这一章,主要探索三个问题:
1.怎样使用unity Profiler去收集剖析数据
2.如何分析profiler数据中的性能瓶颈
3.隔离性能问题和确定根源问题的技巧

Unity Profiler
untiy Profiler在untiy编辑器中,它通过生成unity3d子系统的实时的使用和统计报告, 为我们提供了有效的方式来缩小查找范围。不同的子系统可以收集的数据如下所列:

  • Cpu消耗
  • 基本和详细的渲染和GPU信息
  • 实时内存分配和总的消耗
  • Audio数据使用
  • 物体引擎(2D,3D)使用
  • 网络消息和操作使用
  • video回放使用
  • 基本和详细的用户接口性能
  • 全局光照统计

当unity项目在Development 模式下编译时, 一些附加的编译标志会生成一些特殊的事件,用来获取log并保存到Profiler中。自然的, 就会引起额外的CPU和内存消耗。更坏的情况出现在untiy editor下使用profiler, 将会消耗更多的CPU和内存,比如编译器更新接口, Scene 窗口, 处理后台任务等,这些额外的消耗是不可忽略的。在一些超大的项目中,当开始profiler的时候,会引起不确定的行为。当实时的深度分析代码的行为时是要付出代价的,我们应该时刻意识到这一点。
在进行分析前,首先进行一些简单的测试,将游戏运行在目标平台上,来收集初始数据和演示一些测试场景。这个方法一般被称为基准法, 最重要的指标有FPS,总内存消耗, CPU的使用轨迹(一般找高峰), CPU/GPU的温度.
在Editor下的数据不能作为基准数据,因为会有额外的开销,这样的数据可能会误导我们, 并且隐藏一些真实环境下游戏的潜在的条件。因为我们应该在目标平台上运行游戏来进行数据分析。
在Editor下的运行结果会更快, 尤其是在处理声音文件,prefabs,脚本对象时,因为Editor会缓存下以前导入的数据,这样再次访问就会更快。

在不同的平台下连接profiler
可以参考官方手册,有在不同的平台的使用方法。

Profiler window基本介绍

主要分为四大部分:
1.profiler控制面板
2.Timeline视图
3.故障视图控制面板
4.故障视图
具体的介绍在官网上有介绍,这里只介绍一些特殊的。
Deep Profiler
通常情况下profiler只记录一般的untiy回调方法的时间和内存分配,比如Awake(),start(), update(), Fixedupdate()。使用Deep Profiler,会重新编译我们的代码,使profiler能够记录每一个调用方法。这也会导致巨大的运行开销,使用更多的内存来存储整个调用堆栈的数据。所以,不能用在大的项目上,可能会导致项目还没运行unity内存就超了,建议用在小的测试场景中,而且由于需要重编代码,在测试的时候,不要来回关闭和开启deep profiler。
可以使用手动模式,利用Profiler.BeginSample, Profiler.EndSample, 来分析代码区域,这样开销会更小。

Profile Editor
profiler editor选项,表示收集unity Editor自己的分析数据,一般用来分析enditor脚本的性能。同时connected player也要设置到Editor选项。

Connected Player
Connected player的下拉框可以选择想要分析的目标,可以是当前的Editor, 可以是本地的游戏的Pc包, 也可以是在远程设备上。

Timeline View

用来展示运行时刻收集的分析数据,并在一系列的区域中显示。每个区域都是unity引擎不同的子系统的数据,并且区域有两部分组成,右侧显示图形化的数据, 左侧是一列类型选项, 勾选后,图形化数据中会添加该类型的数据。当选中Timeline view的某个区域,BreakDown视图会显示当前帧在对应子系统中更加详细的数据。区域可以被移除和添加;

BreakDown view
BreakDown展示的信息依赖于选择的Timeline区域和当前的Breakdown控制面板的一些选项,下面就详细的介绍。

CPU Usage Area
这个区域显示了所有的CPU使用和统计数据。这个区域可能最复杂和最有用的,因为它覆盖了大量的unity子系统, 比如 MonoBehaviour组件,相机、一些渲染和物理过程, 用户接口(包括Editor的接口,如果正在使用Editor运行),声音的流程, Profiler自己等。
在展示cpu使用时,在Breakdown view可以有三种模式可以选
1.Hierarchy mode 层次结构模式
2.Raw Hierarchy Mode 原生层次结构
3.Timeline Mode 时间线视图

层次结构模式,通过将相似的数据元素和全局unity函数调用进行分组,能够很方便的展示调用堆栈的调用。比如,渲染分隔用到的BeginGUI(), EndGUI()调用,它们被分到一个组中;这种模式可以在最开始的时候就直观反应出哪个函数调用占用最多的CPU时间。
原生的层次结构,和层次结构类似,不过它没有将全局unity函数组合在一起,而且进行隔离。这使得breakdow视图更难观察,不过在我们想要统计特殊的全局方法的调用次数时,就显得更方便了。还是Begingui, endgui(), 会被分到不同的组中,可以更清晰的看到每个的调用次数。
timeline模式可能是查看CPU使用的最有效的方式, 它通过调用过程中调用堆栈的扩展和收缩来展示当前帧的CPU使用情况。breakdown的垂直部分分成不同的区域,显示不同的线程,比如main thread, Render thread , 和各种用来处理载入场景和资源的后台工作线程(job system)。它的水平轴代表了时间,越宽的模块就代表了消耗更多的CPU时间, 水平的尺度也代表了相对时间, 可以方便比较两个函数调用的CPU占有时间。垂直轴代表了调用堆栈, 因此越深的链代表了此时最多的调用。
在timeline模式下,breakdown视图的顶部模块显示的是untiy引擎调用的函数(start(), awake(), Update())。

GPU useage Area
和CPU使用类似,除了展示的调用和时间都是发生在GPU中。 相应的unity方法一般和相机、绘制、透明、几何变换、光照、阴影相关的。

Rendering Area
提供一些通用的渲染统计数据,主要是CPU中发生的为GPU渲染做的准备工作。包括setpass call数量(也叫Draw Call), 渲染场景的batches数量, 动态和静态合批的次数,texture的内存占用等。

Memory Area
检查应用的内存使用情况,在breakdown视图中,可以有两种模式查看:

  1. Simple mode
  2. Detailed mode
    简单模式只提供一些子系统的内存占用的总览数据, 包括unity引擎的底层代码, Mono相关(总的堆尺寸,和使用的堆尺寸, 这部分内存是需要被垃圾回收的), 图形相关的资源, 声音资源和缓存

详细模式展示了单个gameobject和monobehavious的原生和处理的代码的内存占用。不过这些信息,只有手动点击了Take Sample时才出现,这个是唯一的方式来收集信息。

Audio Area
声音的统计,可以用来观察声音系统的CPU使用,和声音源码,声音文件的内存使用情况。声音在性能优化时经常被忽略, 但是声音可能会变成成非常大的瓶颈, 如果没有合适的处理,由于它潜在的硬盘访问次数和CPU的使用,所以不要轻视它。

The Physics 3D and 2D Areas
有两个不同的物理区域,一个是3D物理(nvidia的 PhysX),另一个是2D物理系统。这个区域听了不同的物理统计,比如Rigidbody, Collider等。

UI and Ui detail
差的UI代码的优化,会影响CPU或GPU, 因此我们要注意UI的代码优化策略。

性能分析的常用方法

最好是带着目标去分析,下面是一些checklist:
1.验证目标脚本存在场景中
2.验证脚本在场景中出现的次数正确
3.验证事件的正确顺序
4.减少代码的修改。
5.减少引擎干扰
6.减少外部干扰

确认脚本存在
直接在Hierarchy中查找t:name , 将会查找所有gameobjects包含脚本或派生的脚本。

确认脚本数量
可以创建object次数过多,或者不小心实例化多个object。这些问题就导致冲突或者重复方法调用导致性能瓶颈。这种情况,我们最好写一些初始化的代码来阻止发生,或者写编辑器来提醒。

确认事件执行顺序
Unity帮我们处理游戏循环,unity有固定的函数调用周期, 比如Awake(), Start(), Update, FixedUpdate()都会在特定的时间触发。但是不同的脚本中的Awkae()的执行顺序是不定的。因此我们要注意,一些初始化的过程不能放在不定顺序的调用中。初始化最好放在start中, 它肯定是在Awake()中进行。不过unity了提供了手动设置脚本的执行order,就在inspector视图中Execution order。
如果我们对直接的调用顺序疑惑时,就使用deubg.log来打印出log信息作为参考依据。不过log是开销非常大的,所以在必要的时候使用。

协程一般会用来处理一系列事件时,它的执行依赖于yield。不过最难预估的使用可能是WaitForSeconds,unity引擎是不确定性, 每个周期都有轻微的不同,可能这一秒有60次updates, 下一秒就只有59次。在协程的开始到结束时间内,可能有不定次数的update()调用。因此在使用协程要注意,最好与其他行为解耦。因为这些不确定性,所以在帧同步时,要避免使用协程和mono的update, 而使用帧数来驱动update()。

尽量少的改动代码
在分析的过程中,上面提到了,加log是一个很不错的办法,但是很可能会忘记移除, 就会导致最后调试时占用更多的内存和CPU。所以要利用源码管理工具,svn,或者git, 能够知道每次我们的代码修改记录。而且要充分使用断点调试,不用添加冗余代码,能够跟踪堆栈,变量数据和条件判断。不过在一些必要情况下,还是要加一些条件判断语句,来方便调试。

减少引擎自带特性的干扰
1.经常出现的错误,当我们打开profiler,并使用键盘来操作游戏时, 在用键盘触发时,忘记点击并返回到Editor的Game窗口。如果Profiler是最近点击的窗口, 编辑器会发送键盘事件到profiler,而不是运行的游戏。这样就收集不到信息。如果Game Window在Editor没有激活,没有什么任何东西在game窗口渲染,那么依赖于game window渲染的时间将不会被激活。
2.VSync 垂直同步, 用来匹配游戏帧率与屏幕刷新速度, 垂直同步会影响游戏帧率,并且它的影响会显示在profiler窗口上。会在层次结构中看到WaitForTargetFPS占用很多的CPU时间。所以我们要先排除掉垂直同步,一种就是在cpu分析器中,去掉Vsync的选中框; 我们可以屏蔽掉垂直同步, Edit->Project setting ->Quality ->Dont sync。不过不能在所有平台上都屏蔽。
3.确保Editor的Console的大量log, error, warining不会直接导致性能下降。它们会占有大量的CPU和堆栈,会引起GC。

减少外部干扰
这一条简单但是非常必要, 我们要检查后台的应用,是否占有大量的CPU和内存。如果我们的游戏突然性能差到超出预期,检查系统的任务,看一下CPU,内存,硬盘任务,可能就发现问题了。


如果使用上面的checklist还没有解决性能问题,我们可能需要更深入的分析。为了分析我们代码中的目标区域,提供了两个策略:
脚本控制Profiler
Profiler可以通过Profiler类来控制,这里主要介绍最重要的两个分隔方法,BeginSample(), EndSample() ,在BeingSample()中可以自定义代码的名字, 这样就可以在层次结构中找到它。要注意的时,它只要development模式下才会编译。

一般的CPU分析方法
Profiler只是一个分析的工具,但是我们以后可能使用其他的引擎工作,所以有必要掌握一些独立的分析代码的技巧。
当分析CPU使用时, 我们真正需要的就是准备的计时系统,刚好.NET库提供了Stopwatch类(system.Diagnostics),我们可以start, stop一个stopwatch对象在任何时刻, 可以轻松获取到从start开始到现在经过的时间。但是,这个类不是非常准备,所以我们可以利用多次测试平均结果的方法来减少误差。

 public class CustomTimer  : IDisposable {
    private string m_TimerName;
    private int m_numTest;
    private Stopwatch m_watch;
    public CustomTimer(string name, int num)
    {
       m_TimerName = name;
        m_numTest = num;
        if (m_numTest == 0)
            m_numTest = 1;
        m_watch = Stopwatch.StartNew();
    }

      public void Dispose()
    {
        m_watch.Stop();
        float ms = m_watch.ElapsedMilliseconds;
        UnityEngine.Debug.Log(string.Format("{0} finished : {1:0.00}" + "ms totaol, 
{2:0.000000} ms per-test" + "for {3} tests", m_TimerName, ms, ms / m_numTest, 
m_numTest));
    }
}

使用方法:
 using (new CustomTimer("my test", num))
        {
            for (int i = 0; i < num; i++)
                TestFunction();
        }

注意点:
如果重复的内存访问,将会造成CPU在内存中查找数据更快,因为最近访问过同样的区域,所以平均时间要比实际的更少。
using方法,是一个典型的使用去保证资源在超出生命周期后合理的被销毁。当使用using代码块最后,会自动调用对象的Dispose()方法,需要脚本需要继承IDisposable接口。
通过using和CustomTimer类,我们可以在任何的引擎下来测试我们的代码。
另一个需要关心的问题是应用的启动时间,unity在场景启动时需要时间去从硬盘中载入数据, 初始化复杂的子系统,比如物理和渲染系统。这些可能只需要几秒钟,但是如果测试代码从初始化就开始测试,就会对结果造成很大的影响。所以,如果想要准备的测试代码,需要等到应用稳定运行的时候。
前面提到了,unity的Console窗口开销很大,所以我们在测试期间,不要使用log方法,但是如果我们确实需要详细的数据打印, 就可以缓存log数据,然后再结束的时候打印出来,这样就可以减少内存的消耗。在CustomTimer中我们使用了字符串拼接,这样会导致大量的内存分配和GC。推荐使用stringbuilder。

最后分析思路

从某种角度来看性能优化就是一种减少消耗有效资源的不必要的任务。在使用各种数据收集工具时,可以总结为三个不同的策略:
理解Profiler
了解它的一些特性,限制,优势,可以从中得到更多的信息。比如,要意识到Timeline视图的信息都是相对的,由于相对变换,大的问题也可能只有一个小的峰值。不要认为大的峰值就是有问题。

减少干扰
数据源越多,就需要花费更多的时间去处理和过滤。所以其中一个最好的做法就是减少数据源,在timeline视图,勾选checkbox就可以缩小数据的范围。
将Gameobjects失活也可以阻止数据的采集,如果逐个的处理Gameobject时,性能突然变的更好了,就可以发现这个object是问题根源。

专注于问题
当没有分析的时候,瓶颈重复和可见的存在,这就是个候选的问题。如果有的瓶颈在Profielr的时候一直存在,要记住这个瓶颈可能是我们的测试代码引起的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值