zoukankan      html  css  js  c++  java
  • unity update优化

    http://forum.china.unity3d.com/thread-13968-1-1.html

    Unity有个消息系统,它可以在运行中当发生指定事件时调用你在脚本中定义的那些魔术方法。这是个非常简单和容易理解的概念,特别对新用户来说。只需定义一个这样的Update方法,就能在每帧调用它。

            
    [C#] 纯文本查看 复制代码
    void Update() {
        transform.Translate(0, 0, Time.deltaTime);
    }

            对于一个经验丰富的开发者来说,这代码看起来有点奇怪。
            1.        不清楚这个方法究竟是如何被调用的。
            2.        不清楚当一个场景中有多个对象时,这些方法的调用顺序是什么。
            3.        这种代码风格无法使用Intellisense。

    UPDATE是怎么被调用的

            Unity并没有使用System.Reflection在每次需要调用魔术方法时来定位它们。
            取而代之的是,在首次访问某个类型中的某个MonoBehaviour时,脚本运行时(Mono或IL2CPP)会检查其脚本中是否存在魔术方法,以及相关信息是否已被缓存。如果发现有已登记在册的特定方法,例如,如果一个脚本中定义了Update方法,它将被加入到一个需要每帧都更新的脚本列表中。
            在游戏过程中,Unity只是简单的循环迭代所有的列表然后执行其中的方法。所以,你的Update方法究竟是public还是private并不重要。

    UPDATE方法们的执行顺序是什么

            执行顺序由脚本执行顺序设置(Script Execution Order Settings)(菜单:Edit > Project Settings > Script Execution Order)决定。要手动设置1000个脚本的执行顺序可不能不是什么好主意,但是要微调某个特定脚本的执行顺序还是可以的。当然,未来我们将会提供更加方便的方式来指定执行顺序,比如在代码中使用一个特性(Attribute)。

    无法使用INTELLISENSE        

            我们在Unity中都使用某种类型的IDE编辑C#脚本,它们中的大多数并不喜欢那些完全搞不清何处被调用的魔术方法。这会导致警告以及代码导航困难。
            一些开发者用一个叫BaseMonoBehaviour或差不多名字的抽象类扩展MonoBehaviour,然后在他们的项目中的每个脚本里都扩展这个类。他们在其中写了一些有用的功能以及一堆虚魔术方法:
    [C#] 纯文本查看 复制代码
    public abstract class BaseMonoBehaviour : MonoBehaviour {
        protected virtual void Awake() {}
        protected virtual void Start() {}
        protected virtual void OnEnable() {}
        protected virtual void OnDisable() {}
        protected virtual void Update() {}
        protected virtual void LateUpdate() {}
        protected virtual void FixedUpdate() {}
    }

            这个结构可以使你在代码中使用MonoBehaviour时更有逻辑性,但存在一个小缺点。我打赌你已经发现了……
            你所有的MonoBehaviour都会储存在Unity的内部更新列表里,你所有的脚本都会在每帧里调用所有这些基本上什么也没干的方法!
            有人可能会问为什么会有人关心一个空方法?因为这些从C++到托管C#的调用有成本上的开销。让我们来看看成本为何。

    调用10000个UPDATE

            我为这篇文章在Github上创建了一个示例项目。它有两个场景,可以通过点击设备或在编辑器中按任意键互相切换:
            https://github.com/valyard/Unity-Updates
            (1) 在第一个场景中,使用下面这样的代码创建了10000个MonoBehaviour:
            
            
    [C#] 纯文本查看 复制代码
    private void Update() {
        i++;
    }


            (2) 在第二个场景中,创建了另外10000个MonoBehaviour。不过,不同的是,这个代码中并不是只调用Update,而是像下面这样,加入了一个由Manager脚本在每帧都调用一次的自定义UpdateMe方法。

    [C#] 纯文本查看 复制代码
    private void Update() {
        var count = list.Count;
        for (var i = 0; i < count; i++) list[i].UpdateMe();
    }


            测试项目在两台iOS设备上被编译为Mono以及IL2CPP,发布设置中都设为非开发模式。它们的运行时间记录如下:
            1.        在第一次Update调用时设置一个Stopwatch (在Script Execution Order中配置)
            2.        在LateUpdate时停止Stopwatch
            3.        将获得的计时时间均摊到几分钟上
            
            Unity version: 5.2.2f1
            iOS version: 9.0
            
            

    360反馈意见截图1652082594132115.png (13.69 KB, 下载次数: 9)

    下载附件

    2016-1-14 11:25 上传



            哇!好多时间!测试肯定哪里出了问题!
            实际上,我只是忘了把Script Call Optimization 设为Fast but no Exceptions,但是现在我们能看到这种设置对性能的影响了……至于IL2CPP不必太在意。

    360反馈意见截图16501110628064.png (15.12 KB, 下载次数: 10)

    下载附件

    2016-1-14 11:29 上传



            OK,这样好多了,让我们切换到IL2CPP

    360反馈意见截图164912178482124.png (13.52 KB, 下载次数: 10)

    下载附件

    2016-1-14 11:31 上传



            这里我们发现两件事情:
            1.        这个优化对于IL2CPP同样有用
            2.        IL2CPP仍有改进空间,而且在写这篇文章的同时Scripting 与IL2CPP团队正在努力提高性能。比如,最新的Scripting分支内包含的优化可以让测试运行快35%。
            我一会儿就会介绍Unity在幕后做了些什么,但是现在让我们修改下Manager代码,将它提速5倍!

    接口调用,虚调用以及数组访问

            如果你还未读过这一系列有关IL2CPP的优秀博文,那么你应该在读完本文后马上去看看!
            结果告诉我们,如果你想在每帧里都循环迭代拥有10000个元素的列表,那应该使用数组而不是List,因为这样生成的C++代码会更简单,而数组访问就是要快很多的。
            在下一个测试中,我把List<ManagedUpdateBehavior> 改为了ManagedUpdateBehavior[]。

    360反馈意见截图16450711655965.png (16.73 KB, 下载次数: 10)

    下载附件

    2016-1-14 13:05 上传



            这看起来好多了!
            我在Mono上使用数组运行时间是0.23ms。
            
    解救之道!

            我们发现了从C++调用C#函数较慢,不过让我们再研究下当调用所有这些对象的Update方法时,Unity实际上做了些什么。最简单的方法就是使用Apple Instruments的Time Profiler。
            注意这不是Mono与IL2CPP 的对比测试 — 讨论的大多数内容对Mono iOS构建同样适用。
            我在iPhone6上用Time Profiler启动了测试项目,记录了几分钟的数据,然后选择了一分钟检视一次。从这行代码开始的所有东西我们都很感兴趣:
            void BaseBehaviourManager::CommonUpdate<BehaviourManager>()
            如果你以前没有使用过Instruments,右边是按照执行时间排序的函数,以及它们调用的其他函数。最左边的列是以毫秒为单位的CPU时间,以及这些函数及其调用的函数所占的CPU时间百分比。左边第二列是函数自己的执行时间。注意,在这个实验中Unity并没有将CPU使用完,所以我们能看到在60秒间隔内有10秒的CPU时间花在了我们的Update上。显然,我们关心的是那些执行时间最长的函数。
            我用我疯狂的Photoshop技术,将一些区域做了颜色区分,以便你能明白到底发生了什么。

    360反馈意见截图16600901578958.png (153.18 KB, 下载次数: 11)

    下载附件

    2016-1-14 13:08 上传



    UpdateBehavior.Update()

            
            在中间你能看到我们的Update方法,以及IL2CPP是如何调用它的 ——UpdateBehavior_Update_m18。但是Unity在那之前还做了很多其他事。

    循环迭代所有的Behaviour


            Unity循环迭代所有的Behaviour并执行更新。特殊的迭代类SafeIterator确保了即使移除了列表中的下一项,整个循环也不会中断。仅仅是循环迭代所有已注册的Behaviour就用了9979ms中的1517ms。

    检测调用是否有效


            下一步,Unity做了一堆检测,确保调用的方法是属于某个已激活已初始化且Start方法已调用过的GameObject的。你肯定不希望在Update里销毁一个GameObject时让游戏崩溃,对吧?这些检测花去了整个9979ms中的另外2188ms。

    准备调用方法


            Unity创建了一个ScriptingInvocationNoArgs实例 (代表了一个从原生到托管的调用)以及ScriptingArguments,然后命令IL2CPP虚拟机调用方法(scripting_method_invoke函数)。这一步消耗了整个9979ms中的2061ms。

    Call the method


            scripting_method_invoke函数检测传入的参数是否有效(900ms),然后调用IL2CPP 虚拟机的Runtime::Invoke方法 (1520ms)。开始时,Runtime::Invoke检测方法是否存在 (1018ms)。而后,它调用一个生成的RuntimeInvoker函数获取方法签名(283ms)。接着再依次调用我们的Update函数,根据Time Profiler,这一步花了42ms。
            一个漂亮的彩色表格。

    360反馈意见截图16660114505663.png (61.27 KB, 下载次数: 10)

    下载附件

    2016-1-14 13:14 上传



    托管的更新


            现在让那个我们在Manager测试上使用下Time Profiler。你在屏幕截图上可以看到,还是同样的一些方法(有些方法因为执行时间少于1ms,甚至都没出现),但是大部分的执行时间实际上都花在了UpdateMe函数上(或者说花在了IL2CPP调用它上——ManagedUpdateBehavior_UpdateMe_m14)。另外,IL2CPP还插入了一个null检测,确保我们循环迭代的数组不会为null。
            下面这个图片使用了相同的颜色。

    360反馈意见截图16581117191561.png (120.85 KB, 下载次数: 9)

    下载附件

    2016-1-14 13:16 上传



    所以,你现在怎么看,我们应该忽略那小小的方法调用吗?

    有关测试的几句话

            老实说,这个测试并不是完全公平的。Unity为了防止你的游戏出错或崩溃,做了很多了不起的事:这个GameObject是否已激活?它是否在Update循环中被销毁了?对象上是否存在Update方法?怎么处理在这个Update循环中创建的MonoBehaviour?——我的Manager脚本没有处理这其中任何一项,仅仅是循环迭代了一堆的对象,调用它们的Update而已。
            在真实世界中,Manager脚本可能会更加复杂,执行得更慢。但是,我是个开发者——我知道我的代码要做什么,我架构我的Manger类时,知道可能的行为是什么,什么不会出现在我的游戏中。而不幸的是,Unity并不知道这些。

    你应该怎么做?

            当然这完全视你的项目而定,但实战中碰到一个游戏在单一场景中使用大量需要在每帧都执行一些代码的GameObject的情况并不少见。通常这看起来都是些不起眼的小代码,似乎不会影响到任何东西,但当其数量非常巨大时,调用几千个Update方法的开销将变得显著。这个时候再去修改游戏架构,重构这些对象为Manager样式,可能已经为时已晚。
            你现在有数据了,在你开始下一个项目时考虑下吧。
  • 相关阅读:
    C#中两个不同时间的相加减以及时间比较
    C#中一些报错处理
    C#将DataGridView中的数据导出为EXCEL
    C# tabcontrol的tabpage切换
    C# DataGridView控件中数据导出到Excel
    C#将数据导入到excel中 出现 “object”未包含“get_Range”的定义
    SQL Server数据库的备份与还原以及在项目中是怎样去实现的 (网摘)
    android各组件详解
    刚刚做得一个Android开发教程的专题
    一个Demo让你掌握所有的android控件
  • 原文地址:https://www.cnblogs.com/nafio/p/9136992.html
Copyright © 2011-2022 走看看