Unity Protobuf3 GC 问题(反序列化)

news2025/1/13 13:14:28

背景: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/2064486.html

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

相关文章

Java开发笔记-mysql语句查询指定索引

今天同事遇到一个奇怪的sql查询的问题&#xff1a;一条sql有时候执行素的很快(0.xxs)&#xff0c;有时候执行很慢(20s)&#xff0c;不知道是什么问题. 猜测&#xff1a;1、是不是第一次执行&#xff0c;被mysql缓存了&#xff1f;后面几次直接拿缓存的结果。 2、是不是网络的原…

入门redis

一、安装redis-py库 打开pycharm 在终端中输入 pip install redis 二、连接到redis服务器 import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue)host是 Redis 服务器的主机名或 IP 地址&#xff0c;port是端口号&#xff0c;db是要使用的数据库编…

指纹浏览器VS虚拟机,在跨境电商中用哪个更好?

在当今的数字世界中&#xff0c;隐私和安全变得越来越重要。尤其是跨境电商卖家来说&#xff0c;经常需要网络上执行环境独立的操作&#xff0c;例如账号运营、在线购物、网上银行、社交媒体管理等。 为了保护账号隐私与做好账号防关联隔离&#xff0c;人们经常寻求指纹浏览器…

Windows-Server-2016/2019绕过WindowsDefender

当获得了一个webshell的时候&#xff0c;下一步要反弹个shell回来 在尝试了https://github.com/trustedsec/unicorn独角兽失败之后&#xff0c;找到了一篇使用golang将shellcode注入到内存的文章 Bypassing Antivirus with Golang - Gopher it! | JUMPSEC LABS GitHub - brimst…

9.Java基础概念-面向对象

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 Facts speak louder than words&#xff01; 类和对象 类&#xff…

mmyolo训练模型报错:ValueError: Key img_path is not in available keys解决办法

使用mmyolo训练模型 的时候报错&#xff1a;ValueError: Key img_path is not in available keys. Traceback (most recent call last): File “tools/train.py”, line 123, in main() File “tools/train.py”, line 119, in main runner.train() File “/root/anaconda3/en…

基于Ubuntu22.04 安装SSH服务

安全外壳协议&#xff08;Secure Shell&#xff0c;简称 SSH&#xff09;是一种在不安全网络上用于安全远程登录和其他安全网络服务的协议。 SSH 由 IETF 的网络小组&#xff08;Network Working Group&#xff09;所制定&#xff0c;SSH 为建立在应用层基础上的安全协议。SSH…

Linux下进程间的通信--管道

关于进程间的通信 Linux进程间通信&#xff08;Inter-Process Communication&#xff0c;IPC&#xff09;是指在多个进程之间传输数据或信号的一些方法。由于Linux中的进程有各自独立的地址空间&#xff0c;因此它们不能直接访问对方的内存。为了实现进程间的通信&#xff0c;…

python探索分形和混沌

简单产生复杂&#xff0c;混沌孕育秩序 0. 引言 a. 分形 fractal 【也叫碎形】 分形是一种具有自相似性和复杂结构的几何图形。在分形结构中&#xff0c;无论放大多少次&#xff0c;局部的结构特征都与整体结构相似。这种特性在自然界中广泛存在&#xff0c;比如树木枝干、山…

目前AI 辅助代码生成是否将成为未来编程的主流?

最强AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频百万播放量https://aitools.jurilu.com/ 我的答案是 肯定的 &#xff01; AI辅助代码生成正在迅速崛起&#xff0c;将会成为未来编程的主流趋势。 当然这里的问题是"…

代码随想录算法训练营第二十三天(回溯 二)

力扣题部分: 39. 组合总和 题目链接:. - 力扣&#xff08;LeetCode&#xff09; 题面: 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以…

Windows系统,查看本机端口被哪个进程占用

1 打开CMD 快捷键 WinR&#xff0c;输入cmd&#xff0c;确定&#xff0c;打开命令行窗口。 2 查看端口的进程PID netstat -aon|findstr "80" 3 根据进程PID查看应用名称 tasklist|findstr "1468" 4 如果想关掉该进程&#xff0c;在任务管理器结束进程既…

「数组」数组双指针算法合集:二路合并|逆向合并|快慢去重|对撞指针 / LeetCode 88|26|11(C++)

目录 概述 1.二路合并 思路 复杂度 Code 2.逆向合并 思路 复杂度 Code 3.快慢去重 思路 复杂度 Code 4.对撞指针 思路 复杂度 Code 总结 概述 数组的线性枚举是我们学习编程时遇到的第一种枚举手段。但是它看起来有点愚蠢&#xff1a;只有一个索引i承担全部…

美食攻略系统---附源码99630

摘要 本论文旨在探讨如何利用SpringBoot技术开发一个美食攻略系统。系统将按照软件开发流程&#xff0c;采用B/S架构和面向对象编程思想进行项目开发。在引言部分&#xff0c;将介绍美食攻略系统的背景和开发目的&#xff0c;后续章节将依据软件开发流程&#xff0c;对系统进行…

电路笔记(PCB):数字信号的带宽与上升沿时间经验公式 BW = \frac{0.35}{T_r}

数字信号的带宽由上升沿决定 1. 传输线路的带宽如果小于信号的带宽就会产生失真 带宽限制和失真&#xff1a;当信号通过带宽受限的传输线路时&#xff0c;如果线路的带宽小于信号的带宽&#xff0c;信号的高频成分将被削弱或完全滤除。这种削弱会导致信号失真&#xff0c;特别…

Luma 1.5正式发布,文生视频加强真实感,时长最长5秒

距离上次版本发布仅过去了两个月&#xff0c;Luma AI再次发布了升级版本Dream Machine 1.5。新版本具有更好的文本到视频转换、更智能地提示理解、自定义文本渲染和改进图像到视频的功能。 得益于Luma AI对所有公众开放&#xff0c;任何人都可以免费试用&#xff0c;平台用户在…

CANoe.DiVa的应用——生成TP层测试用例过程流程详解(二)

🙋‍♂️【Vector CANdelastudio配置CDD】文章合集💁‍♂️点击跳转 ——————————————————————————————————–—— 从0开始学习CANoe使用 从0开始学习车载测试 相信时间的力量 星光不负赶路者,时光不负有心人。 目录 一.概述2.经典CAN T…

基于x86 平台opencv的图像采集和seetaface6的人脸朝向姿态估计功能

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、开发流程3.1 编写测试3.2 配置资源文件3.2 验证功能一、概述 本文档是针对x86 平台opencv的图像采集和seetaface6的人脸朝向姿态估计功能,opencv通过摄像头采集视频图像,将采集的视频图像送给seetaface6的人脸朝向姿态…

JavaEE 第15节 JUC相关组件介绍

目录 前言&#xff1a; Callable ReentrantLock Semaphore CountDownLatch 前言&#xff1a; 在Java中&#xff0c;JUC&#xff08;包路径&#xff1a;java.util.concurrent&#xff09;是一个用于并发编程的包&#xff0c;提供了线程安全的集合类、同步工具、并发执行框…

OSPF配置学习笔记

1.OSPF基础配置命令 1.1&#xff08;系统视图&#xff09;创建并运行OSPF进程 [Huawei] ospf [ process-id | router-id router-id ] porcess-id用于标识OSPF进程&#xff0c;默认进程号为1。OSPF支持多进程&#xff0c;在同一台设备上可以运行多个不同的OSPF进程&#xff0…