Unity Protobuf3.21.12 GC 问题(反序列化)

背景:Unity接入的是 Google Protobuf 3.21.12 版本,排查下来反序列化过程中的一些GC点,处理了几个严重的,网上也有一些分析,这里就不一一展开,默认读者已经略知一二了。

如果下面有任何问题请评论区留言提出,我会留意修改的!

GC点1

每次反序列化解析Message的时候,会将Stream传给MessageParser.cs,然后传给MessageExtensions.cs,这里每次都会new CodeInputStream();造成GC(如下图1,2)

这里的做法是改成了单例Instance,将每处new改成获取单例,然后调用Reset,参考以下部分代码,替换单例的调用代码这里略过(搜引用即可)。

这里有个易错点,Reset的bytes.length值,必须传(0,0),我改成(0,bytes.length)报错了,参考CodedInputStream构造函数本身也是(0,0)。

private static CodedInputStream _bytesInstance;
public static CodedInputStream GetBytesInstance(byte[] buffer, int bufferPos, int bufferSize)
{
    if (_bytesInstance == null)
    {
        _bytesInstance = new CodedInputStream(buffer, bufferPos, bufferSize);
    }
    else
    {
        _bytesInstance.Reset(buffer, bufferPos, bufferSize, true);
    }
    return _bytesInstance;
}
private static byte[] bytes = new byte[BufferSize];
private static CodedInputStream _streamInstance;
public static CodedInputStream GetSteamInstance(Stream input)
{
    if (_streamInstance == null)
    {
        _streamInstance = new CodedInputStream(input);
    }
    else
    {
        _streamInstance.Reset(bytes, 0, 0, false, ProtoPreconditions.CheckNotNull(input, "input"));
    }
    return _streamInstance;
}
private static CodedInputStream _streamBytesInstance;
public static CodedInputStream GetSteamBytesInstance(Stream input, byte[] buffer)
{
    if (_streamBytesInstance == null)
    {
        _streamBytesInstance = new CodedInputStream(input, buffer);
    }
    else
    {
        _streamBytesInstance.Reset(buffer, 0, 0, false, ProtoPreconditions.CheckNotNull(input, "input"));
    }
    return _streamBytesInstance;
}
...
...
...
public void Reset(byte[] buffer, int bufferPos, int bufferSize, bool leaveOpen, Stream input = null)
{
    this.input = input;
    this.buffer = buffer;
    this.state = default;
    this.state.bufferPos = bufferPos;
    this.state.bufferSize = bufferSize;
    this.state.sizeLimit = DefaultSizeLimit;
    this.state.recursionLimit = DefaultRecursionLimit;
    SegmentedBufferHelper.Initialize(this, out this.state.segmentedBufferHelper);
    this.leaveOpen = leaveOpen;

    this.state.currentLimit = int.MaxValue;
}

GC点2

protoc.exe 生成的proto message 的 cs 模板代码,都会带一个Parser给业务方使用,使用Parser来反序列化数据流(下图)

然后仔细看生成的代码(下图),_parser是static readonly,初始化的时候就构造好了,常驻内存,但这里有个延迟初始化,将lambda () => new ToyTrackingSurvivorData() 透传给MessageParser。

我们看看MessageParser做了啥(下图)

这里的ParseFrom是我们业务调过来的,也就是每一次的反序列化,都会factory()一次,GC点无疑了,那么问题已经找到了,需要怎么解决呢。

一开始想的是这里也做成单例,每次factory()改成每次先reset然后再返回,但报错了,错误原因是当.proto里面的字段是repeated或者map的时候,需要同时factory()多个对象出来,这里单例就走不通了,那么就做对象池把。

关于对象池设计的思考:

  1. Protobuf源码里需要有一个池子,每次factory()实例化给出去的对象,业务用完了要回池子,下次业务取的时候优先从池子里面取
  2. Parser每次MergeFrom的时候(这里可以理解为每次业务从池子里取出来的时候),需要把从池子里取出来的对象数据成员都Reset为default,或者Clear数据,这里值类型是default,repeated & map是引用类型,需要Clear,注意:存在proto里面是repeated<message>套repeated<message>再套repeated<int>的情况,所以需要考虑递归去清理。
  3. 因为Parser所在的cs文件是protoc.exe生成的代码,需要改生成模板的代码工具,也就是protoc.exe的源码
  4. 设计业务回收池策略,也就是业务什么时候用完,返给池子

关于第一点我这里踩了个小坑,因为考虑到每个message的类型都不一样,所以需要做Dictionary<className, MObjectPool>的池子,也实现了,但发现每次池子里的个数都是1,才反应过来下面这段代码的设计理念

private static readonly pb::MessageParser<ToyTrackingSurvivorData> _parser = new pb::MessageParser<ToyTrackingSurvivorData>(() => new ToyTrackingSurvivorData());

它通过范型MessageParser<T>生成了无数个_parser<T>,每个message类都一一对应,这样也不需要做Dictionary了,也就是每个Parser都自带一个MObjectPool,代码就简洁多了。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Google.Protobuf
{
    internal interface IObjectPool
    {
        int countAll { get; }                   // 总对象个数
        int countActive { get; }                // 当前活跃对象个数
        int countInActive { get; }              // 当前队列可用对象个数
    }
    internal class MObjectPool<T> : IObjectPool
    {
        private const int LimitNum = 1024;
        private readonly Queue<T> _queue = new Queue<T>(LimitNum);
        private readonly Func<T> _create;
        private readonly Action<T> _get;
        private readonly Action<T> _release;
        private readonly Action<T> _destroy;
        public int countAll { get; private set; }
        public int countActive { get { return countAll - countInActive; } }
        public int countInActive { get { return _queue.Count; } }
        public MObjectPool(Func<T> create, Action<T> get = null, Action<T> release = null, Action<T> destroy = null)
        {
            _create = create;
            _get = get;
            _release = release;
            _destroy = destroy;
        }
        public T Get()
        {
            T t;
            if (_queue.Count == 0)
            {
                t = _create();
                countAll++;
            }
            else
            {
                t = _queue.Dequeue();
            }
            _get?.Invoke(t);
            return t;
        }
        public void Recycle(T t)
        {
            if (t == null) return;
            if (countInActive < LimitNum)
            {
                _queue.Enqueue(t);
            }
            else
            {
                countAll--;
            }
            _release?.Invoke(t);
        }
        public void Destroy()
        {
            if (_destroy != null)
            {
                while(_queue.Count > 0)
                {
                    _destroy(_queue.Dequeue());
                }
            }
            _queue.Clear();
            countAll = 0;
        }
    }
    public class MObjcetPoolMgr<T> where T : IMessage<T>
    {
        private MObjectPool<T> _pool;
        private static MObjcetPoolMgr<T> _instance;
        public static MObjcetPoolMgr<T> Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new MObjcetPoolMgr<T>();
                }
                return _instance;
            }
        }
        public T Get(Func<T> create, Action<T> get = null, Action<T> release = null, Action<T> clear = null)
        {
            if (_pool == null)
            {
                _pool = new MObjectPool<T>(create, get, release, clear);
            }
            var t = _pool.Get();
            //log("Get");
            return t;
        }
        public void Recycle(T t)
        {
            _pool.Recycle(t);
            //log("Recycle");
        }
        public void Destroy()
        {
            _pool.Destroy();
            //log("Destroy");
        }
        private static StringBuilder str = new StringBuilder();
        private void log(string op)
        {
            str.Clear();
            str.Append($"[{nameof(MObjcetPoolMgr<T>)}][{op}] {typeof(T).Name} countAll:{_pool.countAll.ToString()} countActive:{_pool.countActive.ToString()} countInActive:{_pool.countInActive.ToString()}");
            UnityEngine.Debug.Log(str.ToString());
        }
    }
}

关于MessageParser的调用如下(简略版),这样factory()的替代品池子就做好了!

public new T ParseFrom(CodedInputStream input)
{
    //T message = factory();
    T message = _poolGet();
    MergeFrom(message, input);
    return message;
}
private T _poolGet()
{
    return MObjcetPoolMgr<T>.Instance.Get(factory);
}
public void PoolRecycle(T t)
{
    if (t == null) return;
    MObjcetPoolMgr<T>.Instance.Recycle(t);
}
public void PoolDestroy()
{
    MObjcetPoolMgr<T>.Instance.Destroy();
}

 下面关于2 3两点其实是一个问题,就是如何修改protoc.exe生成的模板代码,这里网上的参考资料有一些零碎,我也是拼起来写完的,思路就是在每个message class里加一个MessageClear方法,来清理池子里的数据,然后在每次用的时候,调用下MessageClear()就行了,直接看我的修改

csharp_message.cc

第一处修改:

WriteGeneratedCodeAttributes(printer);
printer->Print("public void MessageClear()\n{\n");
for (int i = 0; i < descriptor_->field_count(); i++){
const FieldDescriptor* fieldDescriptor = descriptor_->field(i);
std::string fieldName = UnderscoresToCamelCase(fieldDescriptor->name(), false);
if (fieldDescriptor->type() == FieldDescriptor::Type::TYPE_MESSAGE || fieldDescriptor->type() == FieldDescriptor::Type::TYPE_GROUP) {
  if (fieldDescriptor->is_repeated()) {
    if (fieldDescriptor->is_map()) {
      if (fieldDescriptor->message_type()->map_value()->type() == FieldDescriptor::Type::TYPE_MESSAGE || fieldDescriptor->message_type()->map_value()->type() == FieldDescriptor::Type::TYPE_GROUP){
        printer->Print("  if($field_name$_ != null) { for (int i = 0; i < $field_name$_.Count; i++) { $field_name$_[i].MessageClear(); } $field_name$_.Clear(); }\n", "field_name", fieldName);
      } else {
        printer->Print("  if($field_name$_ != null) $field_name$_.Clear();\n", "field_name", fieldName);
      }
    } else {
      printer->Print("  if($field_name$_ != null) { for (int i = 0; i < $field_name$_.Count; i++) { $field_name$_[i].MessageClear(); } $field_name$_.Clear(); }\n", "field_name", fieldName);
    }
  } else {
    printer->Print("  if($field_name$_ != null) $field_name$_.MessageClear();\n", "field_name", fieldName);
  }
}
else if (fieldDescriptor->type() == FieldDescriptor::Type::TYPE_BYTES) {
  if (fieldDescriptor->is_repeated()) {
    printer->Print("  if($field_name$_ != null) $field_name$_.Clear();\n", "field_name", fieldName);
  } else {
    printer->Print("  if($field_name$_.Length != 0) $field_name$_ = pb::ByteString.Empty;\n", "field_name", fieldName);
  }
}
else if (fieldDescriptor->type() == FieldDescriptor::Type::TYPE_ENUM){
  if (fieldDescriptor->is_repeated()) {
    printer->Print("  if($field_name$_ != null) $field_name$_.Clear();\n", "field_name", fieldName);
  } else {
    printer->Print(
    "  $field_name$_ = $field_type$.$default_value$;\n", "field_type", GetClassName(fieldDescriptor->enum_type()), "field_name", fieldName, "default_value", GetEnumValueName(fieldDescriptor->default_value_enum()->type()->name(), fieldDescriptor->default_value_enum()->name()));
  }
}
else{
  if (fieldDescriptor->is_repeated()) {
    printer->Print("  if($field_name$_ != null) $field_name$_.Clear();\n", "field_name", fieldName);
  } else {
    printer->Print(
    "  $field_name$_ = $default_value$;\n", "field_name", fieldName, "default_value", "default");
  }
}
}
printer->Print("}\n");

csharp_message.cc

第二处修改:

printer->Print("MessageClear();\n");

csharp_message.cc

第三处修改:

printer->Indent();
printer->Print("MessageClear();\n");
printer->Outdent();

到此,protoc.exe的生成代码就改好了,解决了2 3点的问题!

 接下来是第四点,业务代码的回收策略了,这里比较吃项目,有很多需要手改的地方,但好在也有模板,可以参考下,我们使用了ProtoGen.exe工具生成协议代码,每次协议使用完之后回收进池子就OK了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/871604.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实现 FastCGI

CGI的由来&#xff1a; 最早的 Web 服务器只能简单地响应浏览器发来的 HTTP 请求&#xff0c;并将存储在服务器上的 HTML 文件返回给浏 览器&#xff0c;也就是静态 html 文件&#xff0c;但是后期随着网站功能增多网站开发也越来越复杂&#xff0c;以至于出现动态技 术&…

2020 位示图

2020年网络规划设计师上午真题解析36-40_哔哩哔哩_bilibili 假设某计算机的字长为32位&#xff0c;该计算机文件管理系统磁盘空间管理采用位示图&#xff08;bitmap&#xff09;&#xff0c;记录磁盘的使用情况。若磁盘的容量为300GB&#xff0c;物理块的大小为4MB&#xff0c;…

【网络安全】漏洞挖掘:IDOR实例

未经许可&#xff0c;不得转载。 文章目录 正文 正文 某提交系统&#xff0c;可以选择打印或下载passport。 点击Documents > Download后&#xff0c;应用程序将执行 HTTP GET 请求&#xff1a; /production/api/v1/attachment?id4550381&enamemId123888id为文件id&am…

C语言 | Leetcode C语言题解之第354题俄罗斯套娃信封问题

题目&#xff1a; 题解&#xff1a; int cmp(int** a, int** b) {return (*a)[0] (*b)[0] ? (*b)[1] - (*a)[1] : (*a)[0] - (*b)[0]; }int maxEnvelopes(int** envelopes, int envelopesSize, int* envelopesColSize) {if (envelopesSize 0) {return 0;}qsort(envelopes, …

JVM 有哪些垃圾回收器?

JVM 有哪些垃圾回收器&#xff1f; 图中展示了7种作用于不同分代的收集器&#xff0c;如果两个收集器之间存在连线&#xff0c;则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。 新生代收集器&#xff08;全部的都是复制算法&#xff09;&…

wps题注为表格或图片编号

word中为表格添加题注&#xff1a; 问题&#xff1a;多次或多人编辑导致--序号不能联动更新&#xff08;域代码不一致,如图&#xff09; 所以是否可以批量替换word里的域代码&#xff1f;如果可以这问题就解决了————失败 解决办法&#xff1a; 如图&#xff0c;复制表头&…

协处理器+流水线 (9)

3级流水线 流程&#xff1a; 取指令 译码 执行。 每一个时钟周期都可以执行一个指令。 提高CPU的能力有两种方法&#xff0c; 1 提高时钟频率&#xff0c;造成单位时间内执行的指令更多。 2 减少每条指令的平均指令周期数CPI &#xff0c;CPI我不太懂&#xff0c;但大概的…

2024.8.21 作业

一个服务器和两个客户端聊天 代码&#xff1a; /*******************************************/ 文件名&#xff1a;server.c /*******************************************/ #include <myhead.h> #define SER_IP "192.168.2.7" // 服务器IP #define SER…

C#开发基础之100个常用的C#正则表达式

前言 正则表达式是处理字符串的强大工具&#xff0c;特别是在文本搜索、替换和验证中。本文将100个常用的C#正则表达式进行分类&#xff0c;以帮助我们更快速地找到适合的正则表达式解决方案。 1. 基础匹配 这些正则表达式用于匹配一些基本的字符或字符串模式。 匹配任意字…

Linux信号机制探析--信号的产生

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 &#x1f4da;信号什么是信号&#xff1f;为什么要有信号&#xff1f;查看Linux系统中信号 &#x1f388;信号产生&#x1f4d5;kill…

【计算机网络】网络版本计算器

此前我们关于TCP协议一直写的都是直接recv或者read&#xff0c;有了字节流的概念后&#xff0c;我们知道这样直接读可能会出错&#xff0c;所以我们如何进行分割完整报文&#xff1f;这就需要报头来解决了&#xff01; 但当前我们先不谈这个话题&#xff0c;先从头开始。 将会…

Kubectl命令、初识pod、namespace

文章目录 一、Kubectl简介基础命令1.基本信息命令2.创建和更新资源命令3.删除资源命令4. 查看日志和调试命令5. 端口转发和复制文件命令6. 部署管理命令7. 伸缩命令8. 配置和上下文管理命令9.常用命令 二、Pod简介核心概念pod常见状态调度和初始化阶段容器创建和运行阶段异常状…

Zookeeper服务注册及心跳机制详解

ZooKeeper提供了一种类似于文件目录的结构来保存key值&#xff0c;其提供了四种key类型&#xff0c;分别是持久节点&#xff0c;临时节点&#xff0c;持久有序节点&#xff0c;临时有序节点。其中临时节点的特性是当创建此节点的会话断开时&#xff0c;节点也会被删除。这一特性…

Apache Commons-IO 库

Apache Commons-IO是Apache开源基金组织提供的一组有关IO&#xff08;Input/Output&#xff09;操作的小框架。这个库的主要目的是为了提高IO流的开发效率&#xff0c;减少在进行文件读写、目录遍历等操作时编写的样板代码量。通过使用Commons-IO库&#xff0c;开发者可以更加简…

WT32-ETH01开发板模块,启明云端物联网方案,乐鑫ESP32多样化开发应用

在物联网(IoT)的浪潮中&#xff0c;无线Wi-Fi模块作为连接传统硬件与现代智能网络的桥梁&#xff0c;正逐渐成为智能家居和设备通信不可或缺的一部分。Wi-Fi模块也被称为串口Wi-Fi模块&#xff0c;是一种嵌入式模块&#xff0c;它能够将串口或TTL电平信号转换为符合Wi-Fi无线网…

关于AR在医疗领域创新应用

AR技术在医疗领域创新应用&#xff0c;旨在展示AR技术如何为医疗行业带来革命性的变化&#xff0c;我们可以从以下几个方面入手&#xff1a; 一、引言 随着科技的飞速发展&#xff0c;增强现实&#xff08;AR&#xff09;技术正逐步渗透到医疗领域的各个环节&#xff0c;为患…

蓝桥杯2021第十二届蓝桥杯青少年组省赛试题真题

带我去看题解&#xff01;&#xff01;&#xff01; 带我去看题单&#xff01;&#xff01;&#xff01; 目录 第一题&#xff1a;[2021第十二届蓝桥杯青少年组省赛] 字符串 题目背景 题目描述 输入格式 输出格式 输入输出样例 第二题&#xff1a;[2021第十二届蓝桥杯…

【Docker】安装Docker环境遇到的坑(VirtualBox)

利用vagrant工具在VirtualBox安装CentOS7环境后&#xff0c;安装Docker环境遇到的坑 前期准备工作 1、卸载原有环境 sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engi…

Spire.PDF for .NET【文档操作】演示:创建 PDF 组合

PDF 作品集是一组文件&#xff0c;其中可以包含文本文档、电子表格、电子邮件、图像、PowerPoint 演示文稿和绘图。尽管 PDF 作品集将不同类型的文件组合成一个单元&#xff0c;但其中的每个文件都保留了其原始格式、分辨率和大小。在本文中&#xff0c;您将学习如何使用Spire.…