图形引擎实战:客户端优化之零GC日志系统改造
摘要:Unity项目的优化可以从资源、渲染、内存、脚本、平台等诸多方面考虑实施。而本文主要介绍日志系统的GC可以通过哪些方法来进行优化。这一模块虽小但是在项目开发调试的过程中却极为常见,用得也挺多。通过此模块的优化探索,总结方式应用到其他模块的优化。
1.1 日志系统对于Unity项目的重要性
在Unity项目中,日志系统是开发和调试过程中不可或缺的组成部分。它为开发人员提供了实时的应用程序状态、错误和警告信息。通过适当的日志记录,开发人员可以更轻松地定位和修复问题,提高开发效率。然而,过量或不经优化的日志输出可能会导致性能问题,特别是与垃圾回收相关的问题。
1.2 优化日志造成的GC问题
在日志系统中优化以减少GC问题我们可以用下面的这些常规方法来处理。
1.2.1 控制日志输出级别
通过仅在需要的情况下记录详细日志,可以减少不必要的日志输出。将日志级别划分为调试、信息、警告和错误,并根据当前开发阶段或问题的复杂程度选择适当的日志级别。以此达到减少日志输出的目的,从而减少日志的消耗。
1.2.2 批量日志记录
避免频繁的单条日志记录,而是将多个日志条目收集并一次性写入。这将减少频繁的内存分配和写入操作,从而减少了GC的可能性。
1.2.3 使用对象池
对于频繁创建和销毁的日志对象,可以考虑使用对象池来重用这些对象,从而减少内存分配和垃圾回收的频率。
1.2.4 异步日志记录
将日志记录操作放入单独的线程中,以避免阻塞主线程并减少与主线程竞争资源的情况。这可以通过使用异步日志库或手动实现异步记录来实现。
1.2.5 避免字符串拼接
避免在日志消息中进行频繁的字符串拼接操作,另外就是在需要的时候再进行字符串的拼接操作。比如要输出到控制台,要显示到UI,要打印到文件的时候再进行拼接操作。
1.3 零GC字符串改造的初衷
通过上面的优化手段能达到一定的优化效果,但是不管怎么去控制日志的数量,用池子,还是异步。最后都会存在字符串的拼接操作,而无论是使用string.format还是用StringBuilder的Append,AppendFormat方法都会产生内存分配从而产生GC。那有没有什么方式可以避免字符串拼接的时候不产生GC呢。网上有不少大神做过类似的事,然后也拔过他们的一些代码来测试,最终还是没有达到理想的效果,所以自己来尝试一下。
1.3.1 字符串拼接产生GC的原因
要解决字符串拼接产生的GC问题,那么首先要知道字符串拼接产生GC的原因。在unity中(C#)字符串是不可变的,这意味着一旦创建了一个字符串对象,就无法对其进行修改。当进行字符串拼接操作时,实际上是在内存中创建了新的字符串对象,将原来的字符串内容和新添加的内容复制到新的对象中。原始的字符串对象和中间结果因此成为不再被引用的垃圾对象,最终会由垃圾回收器回收,从而产生GC。
解决了拼接过程要去创建新对象的问题就能避免GC。
1.3.2 零GC字符串实现的过程
1.3.2.1 全局缓存避免频繁内存分配
为了避免格式化字符串过程中创建新的对象,进行新的内存分配,所以我们需要一个全局的buffer来存放格式化的字符,所有的格式化操作都是在向这个buffer中写入东西。
其中的m_Char用来存放格式化的字符,m_Buffer是用来写文件的。
1.3.2.2 动态扩容
在格式化过程可能由于预申请的空间不足而需要重新分配大小,也用来进行初始化操作。
1.3.2.3 字符串格式化
在日志内容的拼接格式化过程中,会出现各种类型的数据进行格式化拼接成一个完整的字符串使用,比如会有INT,FLOAT,SHORT,BYTE,DOUBLE,BOOL以及时间DataTime等类型进行字符串的转换拼接,那为了消除GC,就不能使用类似tostring()的方法去把数字类型的参数转成字符串。那我们就需要自己讲这些类型的参数存放到我们定义的char*的buffer中去。
1.3.2.3.1 整数类型参数转换
整数类型的转换大同小异,我们用最简单的INT类型的参数来进行转换举例。如果INT的值为0那就直接向buffer中写入字符‘0’,非零的整数我们就逐一取出各位的数字存入到buffer中去,我们是从低位往高位逐一取逐一存,所以在buffer中我们要从后往前存放,如果是负数则在数字最前存入‘-’号,然后返回格式化后字符数组的起始索引。代码实现为:
BYTE类型的转换代码:
1.3.2.3.2 浮点数类型转换
FLOAT跟DOUBLE的转换要稍微注意一下,因为存在小数点,所以我们在转换FLOAT跟DOUBLE的时候需要把参数看成3部分来处理及整数部分小数点和小数部分,实现代码如下:
1.3.2.3.3 BOOL类型转换
BOOL类型的参数的值只有TRUE跟FALSE,所以我们可以直接从后往前向buffer中写入TRUE或者FALSE,代码如下:
1.3.2.3.4 DataTime类型转换
时间类型的参数要稍微复杂一点,因为参数的内容较多,其实咱也可以化复杂为简单,我们需要的时间格式是由时分秒3部分组成中间用冒号隔开,那么我们拿到时间类型参数的时候获取到时分秒的值以后,分开处理即可,代码如下:
1.3.2.4 字符数组连接
参数与参数之间进行拼接的时候是产生GC的又一个地方,为了避免GC产生我们就要用到前面定义的全局缓存,将我们转换好的字符数组写入缓存中,代码如下:
1.3.2.5 格式化追加
当有多类型多参数的时候,我们需要使用格式化来将多参数拼接起来。由于参数的个数不同,所以需要根据参数的个数来重载多个AppendFormat函数。具体代码展示其中一个即可,就以含有两个参数的格式化操作为例,代码如下:
1.3.2.6 写入数据流
拼接好的字符数组,也就是最终的日志内容,我们需要写入到文件中,那我们可以直接使用二进制写入器把拼接好的内容写进去:
1.3.2.7 Tostring方法
重载了ToString的方法,用来获得string类型的变量,这个过程会产生GC。
1.3.3 零GC字符串在日志中的使用
初始化全局变量:
使用FoolStringBuilder的AppendFormat和Append方法来进行格式化操作:
1.3.4 零GC日志测试
1.3.4.1 测试用例
在游戏主线程update方法中每帧写入5条日志内容:
1.3.4.2 测试结果
在profier中查看结果:
日志写入文件情况:
作为对比在,同样的测试用例测试使用string和stringbuilder实现的日志来写日志,结果如下:
String实现的日志写同样的日志每帧有305KB的GC。
Stringbuilder实现的日志系统写同样数量的日志,每帧180KB的GC。
1.4 总结
通过使用自定义字符串拼接格式化方案,确实将由传统字符串格式化产生的GC给清零了。零GC字符串不仅仅可以使用在日志系统,在项目其他需要使用字符串格式化的地方也可以扩展使用,虽然最后在ToString的时候会产生一些GC,但是至少能节约掉格式化过程产生的GC。
最后就是该优化操作是针对项目日志系统的特定优化,在类型支持上可能会存在不全面的问题。
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com
#牛客解忧铺##我的成功项目解析##引擎开发工程师##游戏开发#