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

news2024/11/25 12:42:33

背景: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.coloradmin.cn/o/2061395.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kubernetes中如何对etcd进行备份和还原】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

不同路径

不同路径 思路&#xff1a; 法一&#xff1a;动态规划 const int N 110; class Solution { int dp[N][N];//dp[i][j]&#xff1a;从起点走到 i j的路径个数。 public:int uniquePaths(int m, int n) {for(int i1;i<n;i){dp[1][i]1;} for(int i1;i<m;i) dp[i][1]1;f…

day36.动态规划+重载操作符

动态规划好难啊(ಥ﹏ಥ) 终于搞懂0-1背包问题的二维数组转一维数组优化的问题了。如图所示: 将二维数组转换成一位数组的核心就是&#xff0c;dp[i][j]选取时&#xff0c;他的值只与dp[i-1][j]&#xff0c;也就是上一行有关&#xff0c;所以可以引出使用一维数组代替二维数组…

python 使用宝塔面板在云服务器上搭建 flask

打开宝塔面板到【网站】&#xff0c;选择【python项目】&#xff0c;点【添加python项目】 填上相关信息&#xff1a; 注意&#xff1a;项目端口是你打算在外网用来访问flask的端口号 勾选【放行端口】&#xff0c;并提交 到阿里云里&#xff0c;选择安全组 手动添加放行端口…

datawind可视化查询-其他函数

飞书文档学习链接:https://www.volcengine.com/docs/4726/47275 1. 用户名函数 用户名函数并非 ClickHouse 官方函数,而是与项目用户信息相结合,用于返回当前使用用户的指定信息的函数。 USERNAME()可返回当前用户的用户名,如下所示。该函数也可与其他函数组合使用 2. J…

51 无显式主键时 mysql 增加的 DB_ROW_ID

前言 这里主要是 探讨, 在我们创建了一个 无主键的数据表, 然后 mysql 会为我们增加的这一个 DB_ROW_ID 的相关 新建一个无主键字段的数据表如下 CREATE TABLE implicit_id_table (username varchar(16) DEFAULT NULL,age int(11) DEFAULT NULL ) ENGINEInnoDB DEFAULT CH…

MySQL范围分区分区表

什么是范围分区分区表&#xff1f; 范围分区是一种根据某个列的范围值来分割表数据的分区方式。在范围分区中&#xff0c;每个分区都有自己的范围条件&#xff0c;当插入数据时&#xff0c;MySQL会根据指定的范围条件将数据分配到相应的分区中。这种分区方式可以使得表的数据按…

2024前端面试题-css篇

1.p和div区别 p自带有一定margin-top和margin-bottom属性值&#xff0c;而div两个属性值为0&#xff0c;也便是两个p之间有不一定间距&#xff0c;而div没有。 2.对css盒模型的理解 标准盒模型&#xff1a;content不包括padding、border、margin ie盒模型&#xff1a;conten…

关于我的生信笔记开通《知识星球》

关于知识星球 1. 为什么到现在才开通《知识星球》 从很早关注我的同学应该了解小杜的知识分享历程&#xff0c;小杜是从2021年11月底开始进入此“坑”&#xff0c;一直坚持到现在&#xff0c;马上3年了&#xff08;24年11月底到期&#xff09;。自己也从一个小青年&#xff0…

【图文并茂】ant design pro 如何统一封装好 ProFormSelect 的查询请求

你仔细看上面的图片吧 经常有这样的需求吧。 这些列表都是查询出来的。 后端 你的后端必须要有 api 。 const getUsers handleAsync(async (req: Request, res: Response) > {const { email, name, live, current 1, pageSize 10 } req.query;const query: any {};…

的卢易表:批量处理Excel数据的自动化工具

的卢易表&#xff1a;批量处理Excel数据的自动化工具 简介 的卢易表是一个可以批量批量处理Excel数据的自动化工具。 自动化是其最大的特点&#xff0c;因为它可以根据配置好的选项自动处理excel数据。 批量是它另一个特点&#xff0c;因为可以做到自动化&#xff0c;所以你可…

JavaScript语法基础之DOM基础

目录 1. DOM 基础 1.1. DOM 是什么&#xff1f; 1.1.1. DOM 对象 1.1.2. DOM 结构 1.2. 节点类型 1.3. 获取元素 1.3.1. getElementById() 1.3.2. getElementsByTagName() 1.3.3. getElementsByClassName() 1.3.4. getElementsByName() 1.4.如何去操作对象 修改属性…

代驾系统源码开发中的用户体验优化:从设计到实现的全方位解析

在当今数字化时代&#xff0c;代驾服务已经成为城市生活中不可或缺的一部分。为了帮助开发者和企业快速搭建代驾服务平台&#xff0c;许多开源的代驾系统源码应运而生。这些源码不仅节省了开发时间&#xff0c;还为进一步的定制化开发提供了坚实的基础。本文将以“开源代驾系统…

Git使用——将GitHub设置成Token

GitHub提供了一种授权方式&#xff0c;使用Token来代替用户名和密码进行身份验证&#xff1b; 下面是将GitHub设置成Token的方法和操作流程&#xff1b; 一、登录GitHub账户 1. GitHub官网&#xff1a;https://github.com 2. 点击右上角的“Sign in”按钮&#xff0c;输入Gi…

遗传算法与深度学习实战(7)——使用遗传算法解决N皇后问题

遗传算法与深度学习实战&#xff08;7&#xff09;——使用遗传算法解决N皇后问题 0. 前言1. N 皇后问题2. 解的表示3. 遗传算法解决 N 皇后问题小结系列链接 0. 前言 进化算法 (Evolutionary Algorithm, EA) 和遗传算法 (Genetic Algorithms, GA) 已成功解决了许多复杂的设计…

Leetcode JAVA刷刷站(74)搜索二维矩阵

一、题目概述 二、思路方向 要在一个满足上述条件的矩阵中查找一个整数 target&#xff0c;我们可以利用矩阵的排序和递增特性来优化搜索过程。由于矩阵的每一行都是非严格递增的&#xff0c;且后一行的第一个元素大于前一行的最后一个元素&#xff0c;我们可以将矩阵视为一个…

Enhancing Octree-Based Context Models for Point Cloud Geometry Compression 论文笔记

1. 论文基本信息 发布于&#xff1a; IEEE SPL 2024 2. 创新点 分析了基于 one-hot 编码的交叉熵损失函数为什么不能准确衡量标签与预测概率分布之间的差异。介绍了 ACNP 模块&#xff0c;该模块通过预测占用的子节点数量来增强上下文模型的表现。实验证明了ACNP模块在基于八…

Linux --- 文件系统

1. 文件系统的概念 Linux 文件系统是一种用于管理、存储和组织数据的层次结构&#xff0c;用于在 Linux 操作系统中管理磁盘上的数据存储。它定义了如何在存储介质&#xff08;如硬盘、固态硬盘或 USB 闪存&#xff09;上组织文件和目录&#xff0c;以及如何读取、写入和操作这…

【时间序列预测_python_jupyter】使用neuralforecast包在jupyter-lab上预测并绘图

neuralforecast包有很多引入好的时间序列预测算法模型&#xff0c;可以直接通过接口调用。 支持的算法模型有&#xff1a; __all__ [RNN, GRU, LSTM, TCN, DeepAR, DilatedRNN,MLP, NHITS, NBEATS, NBEATSx, DLinear, NLinear,TFT, VanillaTransformer, Informer, Autoforme…

wsl2 airsim wairing for connect (Windows11 UE4.27)问题解决

一、概述 这里记述我遇到我在使用wsl2子系统与Windows11上进行交互时候&#xff0c;遇到的一些我之前没有遇到过的问题。 之前的我写的配置链接在这里。 UE5 with plugins AirSim in Windows & ROS in WSL2-Ubuntu 20.04配置过程记录_airsim ue5-CSDN博客文章浏览阅读455次…