Skip to content

Commit

Permalink
# protobuf 性能优化及 GC 优化总结
Browse files Browse the repository at this point in the history
## protobuf-net 分析

[protobuf-net](https://github.com/protobuf-net/protobuf-net) 当前的 GC 问题有:

- 序列化
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`property.GetValue(value, null)`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`Tail.Write(value, dest)`
  - foreach。`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`
- 反序列化GC
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`property.SetValue`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`Tail.Read(oldVal, source)`
  - 列表创建。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`value = Activator.CreateInstance(concreteType)`
  - 列表扩容。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`list.Add`
  - byte[]创建。函数`ProtoBuf.ProtoReader.AppendBytes`中的字节数组创建
  - Pb对象创建。函数`ProtoBuf.Serializers.TypeSerializer.Read`中的`CreateInstance(source)`

## protobuf-net-gc-optimization 针对 protobuf-net 进行的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization)

- 去反射:对指定的协议进行Hook。
  - 反射产生的地方在protobuf-net的装饰类中,具体是PropertyDecorator,没有去写工具自动生成Wrap文件,而是对指定的协议进行Hook。`CustomDecorator`, `ICustomProtoSerializer`及其实现
- foreach
  - foreach 对列表来说改写遍历方式,没有对它进行优化,因为 Unity 5.5 以后版本这个问题就不存在了
- 无GC装箱
  - 消除装箱操作。重构代码,而 protobuf-net 内部大量使用了object 进行参数传递,这使得用泛型编程来消除 GC 变得不太现实。实现了一个无 GC 版本的装箱拆箱类`ValueObject`
- 使用对象池
  - 对于 protobuf-net反序列化的时候会创建pb对象这一点,最合理的方式是使用对象池,Hook 住protobuf-net 创建对象的地方,从对象池中取对象,而不是新建对象,用完以后再执行回收。`IProtoPool`及实现
- 使用字节缓存池
  - 对于 new byte[] 操作的 GC 优化也是一样的,只不过这里使用的缓存池是针对字节数组而非 pb 对象,实现了一套通用的字节流与字节 buffer 缓存池`StreamBufferPool`,每次需要字节buffer时从中取,用完以后放回。

### protobuf-net-gc-optimization 的其他优化

- BetterDelegate:泛型委托包装类,针对深层函数调用树中使用泛型委托作为函数参数进行传递时代码编写困难的问题。
- BetterLinkedList:无GC链表
- BetterStringBuilder:无GC版StrigBuilder
- StreamBufferPool:字节流与字节buffer缓存池
- ValueObject:无GC装箱拆箱
- ObjPool:通用对象池

关键节点:

- LinkedList当自定义结构做链表节点,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、Find、FindLast每次都有GC产生
- 所有委托必须缓存,产生GC的测试一律是因为每次调用都生成了一个新的委托
- List<T>对于自定义结构做列表项,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、IndexOf、sort每次都有GC产生;对于Sort,需要传递一个委托。这两点的实践上面都已经说明。

## 针对 protobuf-net-gc-optimization 的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization) 使用的`protobuf-net`是 2015 年之前的版本,当前项目使用的是 protobuf-net 2.4.5, 把 protobuf-net-gc-optimization 相关的优化合并到了 protobuf-net 2.x 版本上。

### foreach

`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`

因为 C# 不支持泛型协变,上述 foreach 循环还会产生GC,需要针对性的优化。

优化前:

```csharp
foreach (object subItem in (IEnumerable)value)
{
    if (checkForNull && subItem == null) { throw new NullReferenceException(); }
    Tail.Write(ValueObject.TryGet(subItem), dest);
}
```

优化后:

```csharp
if (value is IList list)
{
    for (int i = 0; i < list.Count; i++)
    {
        var subItem = list[i];
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
else
{
    foreach (object subItem in (IEnumerable)value)
    {
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
```

### 其他优化

`protobuf-net`的`BufferPool`在(2018.6.8)[https://github.com/protobuf-net/protobuf-net/commit/9718b9221ee0c2aa13509d0a258a0728d3fc3210#diff-3df8aa4e7ab0d7118f25612197fbe78d]修改为`弱引用(WeakReference)`实现内部缓存,之前的[老版本]为(https://github.com/protobuf-net/protobuf-net/commit/15fa224b3ceab2cdf99012d999307b3435936665#diff-3df8aa4e7ab0d7118f25612197fbe78d)。弱引用会导致 Unity Profile 时,每次调用缓存失效,创建新的对象(56B)。这里使用老的版本。

## 需要再次确认的代码

- `ProtoBuf.BufferPool`内部有锁,用于 ProtoWriter,ProtoReader 读写, 能否优化掉
- `ProtoBuf.ProtoReader.AppendBytes`: `protobuf-net-gc-optimization`的注释(// TODO:这里还有漏洞,但是我们目前的项目不会走到这)需要再次确认

## 测试结果

`Assets/TestScenes/TestProtoBuf/TestProtoBuf.unity` 及 `TestProtoBuf.Test5` 测试结果, 开启 deep profile:

|             | 序列化 GC/time | 反序列化GC GC/time |
| ----------- | -------------- | ------------------ |
| 优化前₁     | 0.8k/0.21ms    | 1.3k/0.23ms        |
| 优化后₂     | 80B+56B/0.23ms | 0B+56B/0.20ms      |
| 再次优化后₃ | 0B             | 0B                 |

1. https://github.com/protobuf-net/protobuf-net 代码
2. https://github.com/smilehao/protobuf-net-gc-optimization 修改合并到 protobuf-net 2.4.5 后。两次List枚举器的获取,每次40B
3. 2.4.5-gc-optimization 代码

## google protobuf 分析

[protocolbuffers/protobuf 3.13.0](https://github.com/protocolbuffers/protobuf/tree/v3.13.0)需要 .NET Standard 2.1,Unity 只支持 .NET Standard 2.0,需要添加额外的DLL[1](protocolbuffers/protobuf#7668), [2](protocolbuffers/protobuf#7252

## 参考资料

- [九:Unity 帧同步补遗(性能优化)](https://zhuanlan.zhihu.com/p/39478710)
- [Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践](https://www.cnblogs.com/SChivas/p/7898166.html)
- [Google protobuf 重用缓存方法](protocolbuffers/protobuf#644)
- [unity 官方 GC 优化教程 - Fixing Performance Problems - 2019.3](https://learn.unity.com/tutorial/fixing-performance-problems-2019-3?uv=2019.3#)
  • Loading branch information
qingfei committed Sep 10, 2020
1 parent 154cebb commit f61489b
Show file tree
Hide file tree
Showing 66 changed files with 4,872 additions and 214 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ _ReSharper.Caches/
BenchmarkDotNet.Artifacts/
filedata.bin
*.binlog
src/VBTest/*
src/VBTest/*
.idea
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# GC optimization

[GC optimization](src/protobuf-net/README.md)

# protobuf-net
protobuf-net is a contract based serializer for .NET code, that happens to write data in the "protocol buffers" serialization format engineered by Google. The API, however, is very different to Google's, and follows typical .NET patterns (it is broadly comparable, in usage, to XmlSerializer, DataContractSerializer, etc). It should work for most .NET languages that write standard types and can use attributes.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using battle;
using CustomDataStruct;
using ProtoBuf.Serializers;
using System.IO;

#if UNITY_EDITOR
using UnityEngine;
#endif
/// <summary>
/// 说明:ProtoBuf初始化、缓存等管理;序列化、反序列化等封装
///
/// @by wsh 2017-07-01
/// </summary>
public class ProtoBufSerializer : Singleton<ProtoBufSerializer>
{
ProtoBuf.Meta.RuntimeTypeModel model;

public override void Init()
{
base.Init();

model = ProtoBuf.Meta.RuntimeTypeModel.Default;
AddCustomSerializer();
AddProtoPool();
model.netDataPoolDelegate = ProtoFactory.Get;
model.bufferPoolDelegate = StreamBufferPool.GetBuffer;
#if UNITY_EDITOR
Debug.Log("init ProtoBufSerializer");
#endif
}

public override void Dispose()
{
model = null;
ClearCustomSerializer();
ClearProtoPool();
}

static public void Serialize(Stream dest, object instance)
{
ProtoBufSerializer.instance.model.Serialize(dest, instance);
}

static public object Deserialize(Stream source, System.Type type, int length = -1)
{
return ProtoBufSerializer.instance.model.Deserialize(source, null, type, length, null);
}

void AddCustomSerializer()
{
// 自定义Serializer以避免ProtoBuf反射
CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data), new NtfBattleFrameDataDecorator());
CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data.one_slot), new OneSlotDecorator());
CustomSetting.AddCustomSerializer(typeof(ntf_battle_frame_data.cmd_with_frame), new CmdWithFrameDecorator());
CustomSetting.AddCustomSerializer(typeof(one_cmd), new OneCmdDecorator());
}

void ClearCustomSerializer()
{
CustomSetting.CrearCustomSerializer();
}


void AddProtoPool()
{
// 自定义缓存池以避免ProtoBuf创建实例
ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data), new NtfBattleFrameDataPool());
ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data.one_slot), new OneSlotPool());
ProtoFactory.AddProtoPool(typeof(ntf_battle_frame_data.cmd_with_frame), new CmdWithFramePool());
ProtoFactory.AddProtoPool(typeof(one_cmd), new OneCmdPool());
}

void ClearProtoPool()
{
ProtoFactory.ClearProtoPool();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

/// <summary>
/// 说明:proto网络数据缓存池需要实现的接口
///
/// @by wsh 2017-07-01
/// </summary>

public interface IProtoPool
{
// 获取数据
object Get();

// 回收数据
void Recycle(object data);

// 清除指定数据
void ClearData(object data);

// 深拷贝指定数据
object DeepCopy(object data);

// 释放缓存池
void Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using battle;
using System;
using System.Collections.Generic;

/// <summary>
/// 说明:proto网络数据缓存池工厂类
///
/// @by wsh 2017-07-01
/// </summary>

public sealed class ProtoFactory
{
static Dictionary<Type, IProtoPool> poolMap = new Dictionary<Type, IProtoPool>();

public static void AddProtoPool(Type protoType, IProtoPool protoPool)
{
#if UNITY_EDITOR
if (poolMap.ContainsKey(protoType)) throw new Exception(string.Format("poolMap already contains key <{0}>!", protoType));
if (protoPool == null) throw new ArgumentNullException("protoPool");
#endif
if (!poolMap.ContainsKey(protoType)) poolMap.Add(protoType, protoPool);
}

public static void RemoveProtoPool(Type protoType)
{
#if UNITY_EDITOR
if (!poolMap.ContainsKey(protoType)) throw new Exception(string.Format("poolMap do not contains key <{0}>!", protoType));
#endif
if (poolMap.ContainsKey(protoType)) poolMap.Remove(protoType);
}

public static void ClearProtoPool()
{
Dispose();
poolMap.Clear();
}

/// <summary>
/// 获取协议数据对象,如果没缓存,则返回null
/// 注意:这个是给PB用的
/// </summary>
/// <param name="protoType"></param>
/// <returns></returns>
public static object Get(Type protoType)
{
object protoData = null;
IProtoPool pool = null;
if (poolMap.TryGetValue(protoType, out pool))
{
protoData = pool.Get();
}
return protoData;
}

/// <summary>
/// 获取协议数据对象,如果没缓存,则创建
/// 注意:游戏逻辑代码中,最好使用这个
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Get<T>() where T : class
{
T protoData = Get(typeof(T)) as T;
if (protoData == null)
{
protoData = Activator.CreateInstance<T>();
}
return protoData;
}

// 说明:除了指定的协议数据,其它数据暂时不走缓存
public static void Recycle(object protoData)
{
// TODO:这里对嵌套协议体的回收有问题,如果上层协议P没定义缓存,而它包含了一个被缓存的子协议体C
// 则在反序列化P时,Protobuf-net会将C从缓存池中取出,而回收时,C没法被回收,表现为缓存失效
// 对于所有的byte[]缓存获取也存在同样的问题
// 如果这样不频繁接受这样的上层协议体(对于发送不影响,每次都是new出来),则绝大时间内不影响缓存池的使用和GC释放
// 现在暂时不做处理,但需要注意******
if (protoData == null) return;
Type protoType = protoData.GetType();
IProtoPool pool;
if (poolMap.TryGetValue(protoType, out pool))
{
pool.Recycle(protoData);
}
}

// 说明:数据深拷贝
public static object DeepCopy(object protoData)
{
if (protoData == null) return null;
Type protoType = protoData.GetType();
IProtoPool pool;
if (poolMap.TryGetValue(protoType, out pool))
{
protoData = pool.DeepCopy(protoData);
}
return protoData;
}

public static void Dispose()
{
var item = poolMap.GetEnumerator();
while (item.MoveNext())
{
item.Current.Value.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using battle;

public sealed class CmdWithFramePool : ProtoPoolBase<ntf_battle_frame_data.cmd_with_frame>
{
protected override void RecycleChildren(ntf_battle_frame_data.cmd_with_frame netData)
{
if (netData.cmd != null)
{
ProtoFactory.Recycle(netData.cmd);
}
}

protected override void ClearNetData(ntf_battle_frame_data.cmd_with_frame netData)
{
netData.server_frame = 0;
netData.cmd = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using battle;

public sealed class NtfBattleFrameDataPool : ProtoPoolBase<ntf_battle_frame_data>
{
protected override void RecycleChildren(ntf_battle_frame_data netData)
{
for (int i = 0; i < netData.slot_list.Count; i++)
{
ProtoFactory.Recycle(netData.slot_list[i]);
}
}

protected override void ClearNetData(ntf_battle_frame_data netData)
{
netData.server_curr_frame = 0;
netData.server_from_slot = 0;
netData.server_to_slot = 0;
netData.slot_list.Clear();
netData.time = 0;
netData.is_check_frame = 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using battle;
using CustomDataStruct;

public sealed class OneCmdPool : ProtoPoolBase<one_cmd>
{
protected override void RecycleChildren(one_cmd netData)
{
StreamBufferPool.RecycleBuffer(netData.cmd_data);
}

protected override void ClearNetData(one_cmd netData)
{
netData.cmd_id = 0;
netData.UID = 0;
netData.cmd_data = null;
}

public override object DeepCopy(object data)
{
one_cmd fromData = data as one_cmd;
if (fromData == null) throw new System.ArgumentNullException("data");

one_cmd toData = ProtoFactory.Get<one_cmd>();
toData.cmd_id = fromData.cmd_id;
toData.UID = fromData.UID;
toData.cmd_data = StreamBufferPool.DeepCopy(fromData.cmd_data);
return toData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using battle;

public sealed class OneSlotPool : ProtoPoolBase<ntf_battle_frame_data.one_slot>
{
protected override void RecycleChildren(ntf_battle_frame_data.one_slot netData)
{
for (int i = 0; i < netData.cmd_list.Count; i++)
{
ProtoFactory.Recycle(netData.cmd_list[i]);
}
}

protected override void ClearNetData(ntf_battle_frame_data.one_slot netData)
{
netData.cmd_list.Clear();
netData.slot = 0;
}
}
Loading

0 comments on commit f61489b

Please sign in to comment.