Unity Protobuf3 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, size = $field_name$_.Count; i < size; 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, size = $field_name$_.Count; i < size; 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->is_repeated()) {printer->Print("  if($field_name$_ != null) $field_name$_.Clear();\n", "field_name", fieldName);} else {if (fieldDescriptor->type() == FieldDescriptor::Type::TYPE_BYTES) {printer->Print("  if($field_name$_.Length != 0) $field_name$_ = pb::ByteString.Empty;\n", "field_name", fieldName);} else if (fieldDescriptor->type() == FieldDescriptor::Type::TYPE_ENUM){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->type() == FieldDescriptor::Type::TYPE_STRING){printer->Print("  $field_name$_ = $default_value$;\n", "field_name", fieldName, "default_value", "\"\"");} 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.rhkb.cn/news/409576.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Web攻防之应急响应(一)

目录 1. 前言 2. 靶场准备 3. 应急场景 4. 应急实战 4.1 查看服务器开放端口 4.2 通过远程链接工具连接服务器 4.3 寻找中间件日志 4.4. 查看并下载日志 4.5 初步分析日志 4.6 查看安全分析报告 4.6 从被篡改的页面开始 4.6 通过修改的文件时间进一步分析日志信息 4.…

如何给10000张图片快速打标签,一招教你节省上千小时!看这期就够了!免费素材管理插件分享

如果给1万张图片打标签&#xff0c;你会怎么做&#xff1f;如果你用过eagle或者billfish的话&#xff0c;那么你一定知道&#xff0c;他们都没有支持用AI来自动给素材打标签。 一旦我们素材多起来的时候&#xff0c;手动输入&#xff0c;键盘都要打冒烟了&#xff0c;效率太低…

企业级NoSql数据库Redis集群

数据库主要分为两大类&#xff1a;关系型数据库与 NoSQL 数据库 关系型数据库 &#xff0c;是建立在关系模型基础上的数据库&#xff0c;其借助于集合代数等数学概念和方法来处理数据库 中的数据主流的 MySQL 、 Oracle 、 MS SQL Server 和 DB2 都属于这类传统数据库。 …

中仕公考怎么样?2025国考报名流程介绍!

现在是八月下旬&#xff0c;距离2025年国考的开始日期越来越近&#xff0c;今天来给大家分享一下国考报名的流程&#xff0c;希望大家提前做个了解。 报考时间(参考去年) 职位表发布&#xff1a;24年10月中旬 网上报名&#xff1a;24年10月中下旬 报名确认&#xff1a;24年…

昂科烧录器支持Melexis迈来芯的位置传感器MLX90365KDC

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中Melexis迈来芯的位置传感器MLX90365KDC已经被昂科的通用烧录平台AP8000所支持。 MLX90365KDC是第II代Triaxis位置传感器IC。 这款单片器件可凭借其表面的集磁点(IMC)&#xf…

ai变声:视频怎么变音?分享6个语音变声器,视频变声不再难!

想过如何让自己的直播内容更吸引人吗&#xff1f;你是否希望通过变声器来打造独特的声音效果&#xff1f;或者&#xff0c;如何用创意声音提升观众的互动体验呢&#xff1f;随着直播行业的不断发展&#xff0c;每位主播都在努力寻找吸引观众的独特方式&#xff0c;而变声器正是…

鸿蒙南向开发:测试框架xdevice核心组件

简介 xdevice是OpenHarmony中为测试框架的核心组件&#xff0c;提供用例执行所依赖的相关服务。 xdevice主要包括以下几个主要模块&#xff1a; command&#xff0c;用户与测试平台命令行交互模块&#xff0c;提供用户输入命令解析&#xff0c;命令处理。config&#xff0c;…

【机器学习】梯度下降算法

梯度下降算法 这篇博客更加详细&#xff0c;以下只是我个人的理解 梯度下降算法原理讲解——机器学习-CSDN博客 梯度下降算法是一种优化算法&#xff0c;通过梯度下降找到函数最小值时的自变量值。 其基本思想是沿着梯度方向的反方向更新参数&#xff0c;直到逼近函数的极值…

DaxPay:一套开源支付网关系统【送源码】

项目介绍 DaxPay是一套开源支付网关系统&#xff0c;已经对接支付宝、微信支付、云闪付相关的接口。可以独立部署&#xff0c;提供接口供业务系统进行调用&#xff0c;不对原有系统产生影响 特色功能 封装各类支付通道的接口为统一的接口&#xff0c;方便业务系统进行调用&am…

wps题注为表格或图片编号

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

uni-app 手记集。

1、uni-app 是一个使用 Vue.js 开发的前端应用的框架&#xff0c;所以不会Vue.js的小伙伴可以先去看看Vue.js的基础教学。 2、.vue文件结构 <template><div class"container"></div> </template><script type"text/ecmascript-6&q…

Code Llama: Open Foundation Models for Code论文阅读

整体介绍 Code Llama 发布了3款模型&#xff0c;包括基础模型、Python 专有模型和指令跟随模型&#xff0c;参数量分别为 7B、13B、34B 和 70B。这些模型在长达 16k tokens 的序列上训练。都是基于 Llama 2。 作者针对infilling (FIM) 、长上下文、指令专门做了微调 long-con…

内网穿透的应用-戴森球计划利用cpolar内网穿透实现好友异地远程联机游戏

文章目录 游戏简介1. 下载MOD2.配置cpolar内网穿透3. 主机开启联机3.1 玩家加入游戏 4. 配置固定的TCP端口5. 游玩体验 游戏简介 《戴森球计划》是一款融合了科幻冒险与经营管理元素的优秀游戏。玩家将在浩瀚宇宙中探索未知星球&#xff0c;建立从零开始的工业帝国&#xff0c…

流媒体服务器如何让WebRTC支持H.265,同时又能支持Web js硬解码、软解码(MSE硬解、WASM软解)

为了这一整套的解决方案&#xff0c;调研研发整整花费了差不多半年多的时间&#xff0c;需达成的目标&#xff1a; 流媒体服务器端不需要将H.265转码成H.264&#xff0c;就能让Chrome解码播放H.265&#xff1b; 注意&#xff1a;现在很多市面上的软硬件通过转码H.265成H.264的…

xss-labs靶场6-10关

第六关 使用a标签&#xff0c;发现a标签可以。 "><a hreFjavascript:alert(aa)>aa</a> 点击aa 第七关 使用双写绕过 1"><ScscriptRipt>alert(1)</ScscriptRipt> 第八关 将javascript:alert(1)进行编码 然后将编码输入再次点击链…

day8JS-作用域

1. 变量的作用域(变量函数) 作用域是变量的可作用范围&#xff0c;变量只有在自己的作用域下才会生效。 函数会产生作用域&#xff0c;在函数内定义的变量只能在函数内使用。 2. 作用域分类 局部作用域&#xff1a; 函数内定义的变量和形参的作用域就是局部作用域&#xff1b;这…

基于SpringBoot的酒店管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot框架技术 工具&#xff1a;IDEA/Eclipse、Navicat、Maven 系统展示 首页 用户注册界面…

rabbitMQ安装与简单demo

安装 mac安装有了brew很方便&#xff0c;windows的可参考 win10 安装rabbitMQ详细步骤 brew install rabbitmq启动 brew services start rabbitmq关闭 brew services stop rabbitmq出了问题之后可以重启一下 brew services restart rabbitmqsome issue 某些库下载超时 比…

学习笔记七:基于Jenkins+k8s+Git+DockerHub等技术链构建企业级DevOps容器云平台

基于Jenkinsk8sGitDockerHub等技术链构建企业级DevOps容器云平台 安装Jenkins在kubernetes中部署jenkins创建名称空间创建pv,上传pv.yaml创建pvc创建一个sa账号通过deployment部署jenkins更新资源清单文件把jenkins前端加上service&#xff0c;提供外部网络访问 配置Jenkins获取…

PeriodWave: Multi-Period Flow Matching for High-Fidelity Waveform Generation

preprintKorea Seoul, Korea 文章目录 abstractmethodFlow Matching for Waveform GenerationHigh-frequency Information Modeling for Flow Matching demo page&#xff0c; PeriodWave 三者最好&#xff0c;而且能把原声中的噪声去掉&#xff0c;GAN一类声码器做不到的。 Pe…