C#代码优化:斩断伸向堆内存的“黑手”

C#代码优化:斩断伸向堆内存的“黑手”

在上期《C#代码优化:拯救你的CPU耗时》中,我们依托UWA本地资源检测,从“时间”的角度对C#代码检测中和CPU耗时相关的知识点为大家进行了简单的讲解。本篇将从“空间”角度入手,为大家继续梳理C#代码检测的相关规则。

C#是在虚拟机(VM)环境当中运行的(Mono虚拟机或IL2CPP虚拟机),其分配的堆内存是由虚拟机进行管理与回收的,因此,C#代码被称为“托管代码”,其堆内存又被称为“托管内存”。托管内存的释放依赖于虚拟机的GC(垃圾回收)机制,本文就不展开讨论了。

C#程序开发要遵循的一个基本原则就是避免不必要的堆内存分配,而堆内存分配主要会造成以下后果:

  • 程序所占用的内存总量过大。内存是有限的资源,对于游戏(特别是移动游戏)来说,内存的占用可谓是寸土寸金。过多的无法释放的内存很可能导致程序崩溃的现象。
  • 过多的分配次数会导致堆碎片变多,从而可能导致无法开辟出所需要的连续的内存,这也会导致程序崩溃。
  • 内存分配会触发GC(垃圾回收),而GC的代价较高,会造成卡顿。

1、该类的方法中存在 .tag的调用

tag是场景中GameObject的标签,而类GameObject的成员tag是一个属性,在获取该属性时,实质上是调用get_tag()函数,从native层返回一个字符串。字符串属于引用类型,这个字符串的返回,会造成堆内存的分配。然而,Unity引擎也没有通过缓存的方式对get_tag进行优化,在每次调用get_tag时,都会重新分配堆内存。

所以当需要对tag进行比较时,我们建议使用函数GameObject.CompareTag(),该函数是在native层实现的,不会造成托管堆内存的分配,也就避免了GC压力。


2、该类的方法中存在对纹理GetPixels()/GetPixels32()调用

对Texture2D类型的对象调用GetPixels()和GetPixels32(),一般都是为了获取指定Mipmap层的全部像素信息,而图片上的像素数量往往是很庞大的。

从内存分配上讲,该函数会在托管堆中分配内存,用以存储纹理数据的像素信息,但引擎不会对其进行缓存。所以如果在频繁调用的函数中使用,就会造成持续性的堆内存分配。

从耗时上讲,擅长执行大规模并行运算的GPU来处理图片信息是非常容易的,但CPU在进行逐个像素信息的获取时,就显得有些吃力了。并且GetPixels()在实现上是由CPU同步执行的,所以耗时会较高,同时会阻塞调用的线程,从而可能会造成卡顿。因此在非必要的情况下,并不建议使用GetPixels()。


3、该类的方法中存在 GetComponentsInChildren调用/该类的方法中存在 GetComponentsInParent调用

在之前的文章《C#代码优化:拯救你的CPU耗时》中,我们对GetComponentsInChildren和GetComponentsInParent进行了简单地讲解。在这里,我们进一步补充说明,这两者在实际调用中,因为涉及到对象的遍历和结果的返回,所以如果使用不当,就会造成持续性的堆内存分配。我们建议开发团队使用接受List类型的引用作为参数的版本,这样就可以避免每次调用都造成堆内存的分配。


4、该类的方法中存在Linq相关函数的调用

Linq相关的函数一般都用于对数据的查询和处理。功能上简单来讲,就是对一堆数据进行各种if判断和for循环处理。使用Linq提供的API,我们可以写出SQL语句风格的代码来进行集合数据的处理,这能够明显提升我们代码的简明性、可读性,维护上也更方便,从而提升编写效率,但是这些优点是以性能的开销为代价的。

Linq在执行过程中会产生一些临时变量,而且会用到委托(lambda 表达式)。如果使用委托作为条件的判定方法,时间开销就会很高,并且会造成一定的堆内存分配。所以在一般的Unity游戏项目开发中,我们不推荐使用Linq相关的函数。在编辑器功能开发中,我们才常常把Linq和Reflection进行配合使用。


5、该类的方法中存在对Renderer进行sharedMaterials的获取

同样,在之前的文章《C#代码优化:拯救你的CPU耗时》中,我们对.material/materials进行了讲解。简单地说,对.material(s)的调用会产生新的材质球实例;而sharedMaterials则是共享材质,不会生成新的材质实例。然而,对.sharedMaterials的调用,依旧会分配堆内存——每次调用都会分配一个用以存放Material的索引的数组。虽然该数组占用的内存相对较小,但我们还是建议不要对其进行频繁地调用。


6、该类的方法中存在 Input.touches调用

移动端项目交互里,点触操作可以说是极为频繁与常见。在点触操作的获取上,Input.touches就是用来获取当前帧中所有点触操作的状态和相应数据。但是查看.touches的实现,我们就会发现:每次在对其调用时,都会new一个数组touches,从而造成一定的堆内存分配。所以开发团队要避免Input.touches的频繁使用以防造成堆内存的额外占用。


7、FindObjectsOfType调用

在之前的文章《C#代码优化:拯救你的CPU耗时》中,我们对FindObjectsOfType进行了简单地介绍。它在增大CPU耗时的同时,也会占用相当一部分的堆内存分配,所以建议通过一次调用,缓存结果的方式减少其带来的性能影响。


8、该类的方法中存在 TextAsset/WWW.bytes调用

该规则针对的其实是两条不相关的Unity API。首先是TextAsset的bytes属性。TextAsset是Unity中的一种文本资源,它支持包括txt、html、bytes和csv等多种格式的文件进行转换。在获取bytes属性时,Unity会从native层获取字节数组(byte[]),从而分配一定的堆内存。

另一个API指的是WWW这个类的bytes成员,每次对其进行调用都会导致堆内存的分配,需要指出的是Unity已经放弃了WWW这个类的相关接口,所以我们建议大家使用UnityWebRequest来实现相关的功能。


以上我们就目前本地资源检测中提供的检测项均做了分析和解读,希望以上这些知识点能在实际的开发过程中为大家带来帮助,同时我们也会基于广大开发者的需求反馈,不断增加新的检测项。

需要说明的是,每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求去设置合适的阈值范围,这也是本地资源检测的一大特点。同时,也欢迎大家来使用UWA推出的本地资源检测服务,可帮助大家尽早对项目建立科学的美术规范。

万行代码屹立不倒,全靠基础掌握得好!

《场景检测:面片、光影和物理属性》
《场景检测:Audio Listener、RigidBody和Prefab连接》
《场景检测:雾效、Canvas和碰撞体》
《特效优化2:效果与性能的博弈》
《特效优化:发现绚丽背后的质朴》
《Prefab优化:预制体中的各种细节选择》

性能黑榜相关阅读

《那些年给性能埋过的坑,你跳了吗?》
《那些年给性能埋过的坑,你跳了吗?(第二弹)》
《掌握了这些规则,你已经战胜了80%的对手!》