Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Unity3D游戏GC优化总结---protobuf-net⽆GC版本优化实践
⼀ protobuf-net优化效果图
protobuf-net是Unity3D游戏开发中被⼴泛使⽤的Google Protocol Buffer库的c#版本,之所以c#版本被⼴泛使⽤,是因为c++版本的源代码不⽀持Unity3D游戏在各个平台上的动态库构建。

它是⼀个⽹络传输层协议,对应的lua版本有两个可⽤的库:⼀个是proto-gen-lua,由tolua作者开发,另外⼀个是protoc,由云风开发。

protobuf-net在GC上有很⼤的问题,在⼀个⾼频率⽹络通讯的状态同步游戏中使⽤发现GC过⾼,所以对它进⾏了⼀次⽐较彻底的GC优化。

下⾯是优化前后的对⽐图:
protobuf-net优化前GC和性能效果图
protobuf-net优化后GC和性能效果图
⼆ Unity3D游戏GC优化概述
有关Unity3D垃圾回收的基本概念和优化策略Unity官⽹有发布过⽂章:。

这篇⽂章讲述了Unity3D垃圾回收机制,和⼀些简单的优化策略,讨论的不是特别深⼊,但是⼴度基本上算是够了。

我罗列⼀下这篇⽂章的⼀些要点,如果你对其中的⼀些点不太熟悉,建议仔细阅读下这篇⽂章:
1、C#变量分为两种类型:值类型和引⽤类型,值类型分配在栈区,引⽤类型分配在堆区,GC关注引⽤类型
2、GC卡顿原因:堆内存垃圾回收,向系统申请新的堆内存
3、GC触发条件:堆内存分配⽽当内存不⾜时、按频率⾃动触发、⼿动强⾏触发(⼀般⽤在场景切换)
4、GC负⾯效果:内存碎⽚(导致内存变⼤,GC触发更加频繁)、游戏顿卡
5、GC优化⽅向:减少GC次数、降低单次GC运⾏时间、场景切换时主动GC
6、GC优化策略:减少对内存分配次数和引⽤次数、降低堆内存分配和回收频率
7、善⽤缓存:对有堆内存分配的函数,缓存其调⽤结果,不要反复去调⽤
8、清除列表:⽽不要每次都去new⼀个新的列表
9、⽤对象池:必⽤
10、慎⽤串拼接:缓存、Text组件拆分、使⽤StringBuild、Debug.Log接⼝封装(打Conditional标签)
11、警惕Unity函数调⽤:、GameObject.tag、FindObjectsOfType<T>()等众多函数都有堆内存分配,实测为准
12、避免装箱:慎⽤object形参、多⽤泛型版本(如List<T>)等,这⾥的细节问题很多,实测为准
13、警惕协程:StartCoroutine有GC、yield return带返回值有GC、yield return new xxx有GC(最好⾃⼰做⼀套协程管理)
14、foreach:unity5.5之前版本有GC,使⽤for循环或者获取迭代器
15、减少引⽤:建⽴管理类统⼀管理,使⽤ID作为访问token
16、慎⽤LINQ:这东西最好不⽤,GC很⾼
17、结构体数组:如果结构体中含有引⽤类型变量,对结构体数组进⾏拆分,避免GC时遍历所有结构体成员
18、在游戏空闲(如场景切换时)强制执⾏GC
三 protobuf-net GC分析
3.1 protobuf-net序列化
先分析下序列化GC,deep profile如下:
打开PropertyDecorator.cs脚本,找到Write函数如下:
1public override void Write(object value, ProtoWriter dest)
2 {
3 Helpers.DebugAssert(value != null);
4 value = property.GetValue(value, null);
5if(value != null) Tail.Write(value, dest);
6 }
View Code
可以看到这⾥MonoProperty.GetValue产⽣GC的原因是因为反射的使⽤;⽽ListDecorator.Write对应于代码Tail.Write,继续往下看:
找到对应源代码:
1public override void Write(object value, ProtoWriter dest)
2 {
3 SubItemToken token;
4bool writePacked = WritePacked;
5if (writePacked)
6 {
7 ProtoWriter.WriteFieldHeader(fieldNumber, WireType.String, dest);
8 token = ProtoWriter.StartSubItem(value, dest);
9 ProtoWriter.SetPackedField(fieldNumber, dest);
10 }
11else
12 {
13 token = new SubItemToken(); // default
14 }
15bool checkForNull = !SupportNull;
16foreach (object subItem in (IEnumerable)value)
17 {
18if (checkForNull && subItem == null) { throw new NullReferenceException(); }
19 Tail.Write(subItem, dest);
20 }
21if (writePacked)
22 {
23 ProtoWriter.EndSubItem(token, dest);
24 }
25 }
View Code
可以看到这⾥的GC是由list遍历的foreach引起的。

继续往内展开,产⽣GC的点全部是这两个原因上。

3.2 protobuf-net反序列化
找到第⼀个产⽣GC的分⽀:
同上述分析,MonoProperty.GetValue、MonoProperty.SetValue产⽣GC原因是反射。

⽽Int32Serializer.Read()代码如下:
1public object Read(object value, ProtoReader source)
2 {
3 Helpers.DebugAssert(value == null); // since replaces
4return source.ReadInt32();
5 }
View Code
可见产⽣GC的原因是因为装箱。

继续往下展开ListDecorateor.Read函数:
由Activator.CreateInstance得出这⾥产⽣GC的原因是实例的创建。

继续往下展开:
GC的产⽣发⽣在List.Add的GrowIfNeeded,可见是列表扩容。

这⾥本质上是因为上⼀步创建了新对象,如果不创建新对象,那么这⾥的list可以⽤Clear⽽⽆须新建,那么就不会有扩容的问题。

继续往下⾯追:
反射和装箱产⽣GC上⾯已经提到,看ProtoReader.AppendBytes代码:
1public static byte[] AppendBytes(byte[] value, ProtoReader reader)
2 {
3if (reader == null) throw new ArgumentNullException("reader");
4switch (reader.wireType)
5 {
6case WireType.String:
7int len = (int)reader.ReadUInt32Variant(false);
8 reader.wireType = WireType.None;
9if (len == 0) return value == null ? EmptyBlob : value;
10int offset;
11if (value == null || value.Length == 0)
12 {
13 offset = 0;
14 value = new byte[len];
15 }
16else
17 {
18 offset = value.Length;
19byte[] tmp = new byte[value.Length + len];
20 Helpers.BlockCopy(value, 0, tmp, 0, value.Length);
21 value = tmp;
22 }
23// value is now sized with the final length, and (if necessary)
24// contains the old data up to "offset"
25 reader.position += len; // assume success
26while (len > reader.available)
27 {
28if (reader.available > 0)
29 {
30// copy what we *do* have
31 Helpers.BlockCopy(reader.ioBuffer, reader.ioIndex, value, offset, reader.available);
32 len -= reader.available;
33 offset += reader.available;
34 reader.ioIndex = reader.available = 0; // we've drained the buffer
35 }
36// now refill the buffer (without overflowing it)
37int count = len > reader.ioBuffer.Length ? reader.ioBuffer.Length : len;
38if (count > 0) reader.Ensure(count, true);
39 }
40// at this point, we know that len <= available
41if (len > 0)
42 { // still need data, but we have enough buffered
43 Helpers.BlockCopy(reader.ioBuffer, reader.ioIndex, value, offset, len);
44 reader.ioIndex += len;
45 reader.available -= len;
46 }
47return value;
48default:
49throw reader.CreateWireTypeException();
50 }
51 }
View Code
可见,这⾥产⽣GC的原因是因为new byte[]操作。

四 Protobuf-net GC优化⽅案
protobuf-net在本次协议测试中GC产⽣的原因总结如下:
1、反射
2、forearch
3、装箱
4、创建新的pb对象
5、创建新的字节数组
下⾯对症下药。

4.1 去反射
⽤过lua的⼈都知道,不管是tolua还是xlua,去反射的⽅式是⽣成wrap⽂件,这⾥去反射可以借鉴同样的思想。

1using CustomDataStruct;
2using ProtoBuf.Serializers;
3
4namespace battle
5 {
6public sealed class NtfBattleFrameDataDecorator : ICustomProtoSerializer
7 {
8public void SetValue(object target, object value, int fieldNumber)
9 {
10 ntf_battle_frame_data data = target as ntf_battle_frame_data;
11if (data == null)
12 {
13return;
14 }
15
16switch (fieldNumber)
17 {
18case1:
19 data.time = ValueObject.Value<int>(value);
20break;
21case3:
22 data.slot_list.Add((ntf_battle_frame_data.one_slot)value);
23break;
24case5:
25 data.server_from_slot = ValueObject.Value<int>(value);
26break;
27case6:
28 data.server_to_slot = ValueObject.Value<int>(value);
29break;
30case7:
31 data.server_curr_frame = ValueObject.Value<int>(value);
32break;
33case8:
34 data.is_check_frame = ValueObject.Value<int>(value);
35break;
36default:
37break;
38 }
39 }
40
41public object GetValue(object target, int fieldNumber)
42 {
43 ntf_battle_frame_data data = target as ntf_battle_frame_data;
44if (data == null)
45 {
46return null;
47 }
48
49switch (fieldNumber)
50 {
51case1:
52return ValueObject.Get(data.time);
53case3:
54return data.slot_list;
55case5:
56return ValueObject.Get(data.server_from_slot);
57case6:
58return ValueObject.Get(data.server_to_slot);
59case7:
60return ValueObject.Get(data.server_curr_frame);
61 }
62
63return null;
64 }
65 }
66 }
View Code
反射产⽣的地⽅在protobuf-net的装饰类中,具体是PropertyDecorator,我这⾥并没有去写⼯具⾃动⽣成Wrap⽂件,⽽是对指定的协议进⾏了Hook。

4.2 foreach
foreach对列表来说改写遍历⽅式就好了,我这⾥没有对它进⾏优化,因为Unity5.5以后版本这个问题就不存在了。

篇⾸优化后的效果图中还有⼀点残留就是因为这⾥捣⿁。

4.3 ⽆GC装箱
要消除这⾥的装箱操作,需要重构代码,⽽protobuf-net内部⼤量使⽤了object进⾏参数传递,这使得⽤泛型编程来消除GC变得不太现实。

我这⾥是⾃⼰实现了⼀个⽆GC版本的装箱拆箱类ValueObject,使⽤⽅式⼗分简单,类似:
1public object Read(object value, ProtoReader source)
2 {
3 Helpers.DebugAssert(value == null); // since replaces
4return ValueObject.Get(source.ReadInt32());
5 }
6public void Write(object value, ProtoWriter dest)
7 {
8 ProtoWriter.WriteInt32(ValueObject.Value<int>(value), dest);
9 }
View Code
其中ValueObject.Get是装箱,⽽ValueObject.Value<T>是拆箱,装箱和拆箱的步骤必须⼀⼀对应。

4.4 使⽤对象池
对于protobuf-net反序列化的时候会创建pb对象这⼀点,最合理的⽅式是使⽤对象池,Hook住protobuf-net创建对象的地⽅,从对象池中取对象,⽽不是新建对象,⽤完以后再执⾏回收。

池接⼝如下:
1///<summary>
2///说明:proto⽹络数据缓存池需要实现的接⼝
3///
4/// @by wsh 2017-07-01
5///</summary>
6
7public interface IProtoPool
8 {
9// 获取数据
10object Get();
11
12// 回收数据
13void Recycle(object data);
14
15// 清除指定数据
16void ClearData(object data);
17
18// 深拷贝指定数据
19object DeepCopy(object data);
20
21// 释放缓存池
22void Dispose();
23 }
View Code
4.5 使⽤字节缓存池
对于new byte[]操作的GC优化也是⼀样的,只不过这⾥使⽤的缓存池是针对字节数组⽽⾮pb对象,我这⾥是⾃⼰实现了⼀套通⽤的字节流与字节buffer缓存池StreamBufferPool,每次需要字节buffer时从中取,⽤完以后放回。

五 protobuf-net GC优化实践
以上关键的优化⽅案都已经有了,具体怎么部署到protobuf-net的细节问题这⾥不再多说,有兴趣的朋友⾃⼰去看下源代码。

这⾥就优化以后的protobuf-net使⽤⽅式做下介绍,⾸先是⽬录结构:
protobuf-net-gc-optimization⼯程结构
1、CustomDatastruct:⾃定义的数据结构
2、Protobuf-extension/Protocol:测试协议
3、Protobuf-extension/ProtoFactory:包含两个部分,其中ProtoPool是pb对象池,⽽ProtoSerializer是对protobuf-net装饰器的扩展,⽤于特定协议的去反射
4、ProtoBufSerializer:Protobuf-net对外接⼝的封装。

主要看下ProtoBufSerializer脚本:
1using battle;
2using CustomDataStruct;
3using ProtoBuf.Serializers;
4using System.IO;
5
6///<summary>
7///说明:ProtoBuf初始化、缓存等管理;序列化、反序列化等封装
8///
9/// @by wsh 2017-07-01
10///</summary>
11
12public class ProtoBufSerializer : Singleton<ProtoBufSerializer>
13 {
14 ProtoBuf.Meta.RuntimeTypeModel model;
15
16public override void Init()
17 {
18base.Init();
19
20 model = ProtoBuf.Meta.RuntimeTypeModel.Default;
21 AddCustomSerializer();
22 AddProtoPool();
23 DataPoolDelegate = ProtoFactory.Get;
24 model.bufferPoolDelegate = StreamBufferPool.GetBuffer;
25 }
26
27public override void Dispose()
28 {
29 model = null;
30 ClearCustomSerializer();
31 ClearProtoPool();
32 }
33
34static public void Serialize(Stream dest, object instance)
35 {
36 ProtoBufSerializer.instance.model.Serialize(dest, instance);
37 }
38
39static public object Deserialize(Stream source, System.Type type, int length = -1)
40 {
41return ProtoBufSerializer.instance.model.Deserialize(source, null, type, length, null);
42 }
43
44void AddCustomSerializer()
45 {
46// ⾃定义Serializer以避免ProtoBuf反射
47 CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data), new NtfBattleFrameDataDecorator());
48 CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data.one_slot), new OneSlotDecorator());
49 CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data.cmd_with_frame), new CmdWithFrameDecorator());
50 CustomSetting.AddCustomSerializer(typeof(one_cmd), new OneCmdDecorator());
51 }
52
53void ClearCustomSerializer()
54 {
55 CustomSetting.CrearCustomSerializer();
56 }
57
58
59void AddProtoPool()
60 {
61// ⾃定义缓存池以避免ProtoBuf创建实例
62 ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data), new NtfBattleFrameDataPool());
63 ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data.one_slot), new OneSlotPool());
64 ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data.cmd_with_frame), new CmdWithFramePool());
65 ProtoFactory.AddProtoPool(typeof(one_cmd), new OneCmdPool());
66 }
67
68void ClearProtoPool()
69 {
70 ProtoFactory.ClearProtoPool();
71 }
72 }
View Code
其中:
1、AddCustomSerializer:⽤于添加⾃定义的装饰器到protobuf-net
2、AddProtoPool:⽤于添加⾃定义对象池到protobuf-net
3、Serialize:提供给逻辑层使⽤的序列化接⼝
4、Deserialize:提供给逻辑层使⽤的反序列化接⼝
使⽤⽰例:
1const int SENF_BUFFER_LEN = 64 * 1024;
2const int REVIVE_BUFFER_LEN = 128 * 1024;
3 MemoryStream msSend = new MemoryStream(sendBuffer, 0, SENF_BUFFER_LEN, true, true);;
4 MemoryStream msRecive = new MemoryStream(reciveBuffer, 0, REVIVE_BUFFER_LEN, true, true);;
5
6 msSend.SetLength(SENF_BUFFER_LEN);
7 msSend.Seek(0, SeekOrigin.Begin);
8
9 ntf_battle_frame_data dataTmp = ProtoFactory.Get<ntf_battle_frame_data>();
10 ntf_battle_frame_data.one_slot oneSlot = ProtoFactory.Get<ntf_battle_frame_data.one_slot>();
11 ntf_battle_frame_data.cmd_with_frame cmdWithFrame = ProtoFactory.Get<ntf_battle_frame_data.cmd_with_frame>();
12 one_cmd oneCmd = ProtoFactory.Get<one_cmd>();
13 cmdWithFrame.cmd = oneCmd;
14 oneSlot.cmd_list.Add(cmdWithFrame);
15 dataTmp.slot_list.Add(oneSlot);
16 DeepCopyData(data, dataTmp);
17 ProtoBufSerializer.Serialize(msSend, dataTmp);
18 ProtoFactory.Recycle(dataTmp);//*************回收,很重要
19
20 msSend.SetLength(msSend.Position);//长度⼀定要设置对
21 msSend.Seek(0, SeekOrigin.Begin);//指针⼀定要复位
22//msRecive.SetLength(msSend.Length);//同理,但是如果Deserialize指定长度,则不需要设置流长度
23 msRecive.Seek(0, SeekOrigin.Begin);//同理
24
25 Buffer.BlockCopy(msSend.GetBuffer(), 0, msRecive.GetBuffer(), 0, (int)msSend.Length);
26
27 dataTmp = ProtoBufSerializer.Deserialize(msRecive, typeof(ntf_battle_frame_data), (int)msSend.Length) as ntf_battle_frame_data;
28
29 PrintData(dataTmp);
30 ProtoFactory.Recycle(dataTmp);//*************回收,很重要
View Code
六 Unity3D游戏GC优化实践
protobuf-net的GC优化实践要说的就这么多,其实做GC优化的⼤概步骤就是这些:GC分析,优化⽅案,最后再重构代码。

这⾥再补充⼀些其它的内容,CustomDatastruct 中包含了:
1、BetterDelegate:泛型委托包装类,针对深层函数调⽤树中使⽤泛型委托作为函数参数进⾏传递时代码编写困难的问题。

2、BetterLinkedList:⽆GC链表
3、BetterStringBuilder:⽆GC版StrigBuilder
4、StreamBufferPool:字节流与字节buffer缓存池
5、ValueObject:⽆GC装箱拆箱
6、ObjPool:通⽤对象池
其中protobuf-net的⽆GC优化⽤到了StreamBufferPool、ValueObject与ObjPool,主要是对象池和免GC装箱,其它的在源代码中有详细注释。

TestScenes下包含了各种测试场景:
测试场景
这⾥对其中关键的⼏个结论给下说明:
1、LinkedList当⾃定义结构做链表节点,必须实现IEquatable<T>、IComparable<T>接⼝,否则Roemove、Cotains、Find、FindLast每次都有GC产⽣
1// 重要:对于⾃定义结构⼀定要继承IEquatable<T>接⼝并实现它
2// 此外:对于Sort,实现IComparable<T>接⼝,则在传⼊委托的时候可以和系统简单值类型⼀样
3public struct CustomStruct : IEquatable<CustomStruct>, IComparable<CustomStruct>
4 {
5public int a;
6public string b;
7
8public CustomStruct(int a, string b)
9 {
10this.a = a;
11this.b = b;
12 }
13
14public bool Equals(CustomStruct other)
15 {
16return a == other.a && b == other.b;
17 }
18
19public int CompareTo(CustomStruct other)
20 {
21if (a != other.a)
22 {
23return pareTo(other.a);
24 }
25
26if (b != other.b)
27 {
28return pareTo(other.b);
29 }
30
31return0;
32 }
33
34// 说明:测试正确性⽤的,不是必须
35public override string ToString()
36 {
37return string.Format("<a = {0}, b = {1}>", a, b);
38 }
39 }
View Code
2、所有委托必须缓存,产⽣GC的测试⼀律是因为每次调⽤都⽣成了⼀个新的委托
1public class TestDelegateGC : MonoBehaviour
2 {
3public delegate void TestDelegate(GameObject go, string str, int num);
4public delegate void TestTDelegate<T,U,V>(T go, U str, V num);
5
6 Delegate mDelegate1;
7 Delegate mDelegate2;
8 TestDelegate mDelegate3;
9 TestTDelegate<GameObject, string, int> mDelegate4;
10 TestDelegate mDelegate5;
11 Comparison<int> mDelegate6;
12 Comparison<int> mDelegate7;
13
14int mTestPriviteData = 100;
15 List<int> mTestList = new List<int>();
16
17// Use this for initialization
18void Start () {
19 mDelegate1 = (TestDelegate)DelegateFun;
20 mDelegate2 = Delegate.CreateDelegate(typeof(TestDelegate), this, "DelegateFun");
21 mDelegate3 = DelegateFun;
22 mDelegate4 = TDelegateFun;
23
24//static
25 mDelegate5 = new TestDelegate(StaticDelegateFun);
26 mDelegate6 = SortByXXX;
27 mDelegate7 = TSortByXXX<int>;
28
29 mTestList.Add(1);
30 mTestList.Add(2);
31 mTestList.Add(3);
32 }
33
34// Update is called once per frame
35void Update () {
36// 不使⽤泛型
37 TestFun(DelegateFun);
38 TestFun(mDelegate1 as TestDelegate); //⽆GC
39 TestFun(mDelegate2 as TestDelegate); //⽆GC
40 TestFun(mDelegate3); //⽆GC,推荐
41 TestFun(mDelegate5); //⽆GC
42// 使⽤泛型,更加通⽤
43 TestTFun(TDelegateFun, gameObject, "test", 1000);//每次调⽤产⽣104B垃圾
44 TestTFun(mDelegate4, gameObject, "test", 1000);// ⽆GC,更通⽤,极⼒推荐***********
45// Sort测试
46 mTestList.Sort();//⽆GC
47 TestSort(SortByXXX);//每次调⽤产⽣104B垃圾
48 TestSort(mDelegate6);//⽆GC
49 TestSort(TSortByXXX);//每次调⽤产⽣104B垃圾
50 TestSort(TSortByXXX);//每次调⽤产⽣104B垃圾
51 TestSort(mDelegate7);//⽆GC
52 }
53
54private void TestFun(TestDelegate de)
55 {
56 de(gameObject, "test", 1000);
57 }
58
59private void TestTFun<T, U, V>(TestTDelegate<T, U, V> de, T arg0, U arg1, V arg2)
60 {
61 de(arg0, arg1, arg2);
62 }
63
64private void TestSort<T>(List<T> list, Comparison<T> sortFunc)
65 {
66 list.Sort(sortFunc);
67 }
68
69private void TestSort(Comparison<int> sortFunc)
70 {
71 mTestList.Sort(sortFunc);
72 }
73
74private void DelegateFun(GameObject go, string str, int num)
75 {
76 }
77
78private void TDelegateFun<T, U, V>(T go, U str, V num)
79 {
80 }
81
82private static void StaticDelegateFun(GameObject go, string str, int num)
83 {
84 }
85
86private int SortByXXX(int x, int y)
87 {
88return pareTo(y);
89 }
90
91private int TSortByXXX<T>(T x, T y) where T : IComparable<T>
92 {
93return pareTo(y);
94 }
95 }
View Code
3、List<T>对于⾃定义结构做列表项,必须实现IEquatable<T>、IComparable<T>接⼝,否则Roemove、Cotains、IndexOf、sort每次都有GC产⽣;对于Sort,需要传递⼀个委托。

这两点的实践上⾯都已经说明。

其它的测试⾃⾏参考源代码。

七 项⽬⼯程地址 gitbub地址为:。

相关文档
最新文档