1.应用背景html
底端设备有大量网络报文(字节数组):心跳报文,数据采集报文,告警报文上报。须要有对应的报文结构去解析这些字节流数据。数组
2.结构体解析
由此,我第一点就想到了用结构体去解析。缘由有如下两点:微信
2.1.结构体存在栈中
类属于引用类型,存在堆中;结构体属于值类型,存在栈中,在一个对象的主要成员为数据且数据量不大的状况下,使用结构会带来更好的性能。网络
2.2.结构体不须要手动释放
属于托管资源,系统自动管理生命周期,局部方法调用完会自动释放,全局方法会一直存在。tcp
3.封装心跳包结构体
心跳协议报文以下:函数
对应结构体封装以下:工具
[StructLayout(LayoutKind.Sequential, Pack = 1)] // 按1字节对齐
public struct TcpHeartPacket
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] //结构体内定长数组
public byte[] head;
public byte type;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
public byte[] length;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Mac;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 104)]
public byte[] data;//数据体
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] tail;
}
4.结构体静态帮助类
主要实现了字节数组向结构体转换方法,以及结构体向字节数组的转换方法。性能
public class StructHelper
{
//// <summary>
/// 结构体转byte数组
/// </summary>
/// <param name="structObj">要转换的结构体</param>
/// <returns>转换后的byte数组</returns>
public static byte[] StructToBytes(Object structObj)
{
//获得结构体的大小
int size = Marshal.SizeOf(structObj);
//建立byte数组
byte[] bytes = new byte[size];
//分配结构体大小的内存空间
IntPtr structPtr = Marshal.AllocHGlobal(size);
//将结构体拷到分配好的内存空间
Marshal.StructureToPtr(structObj, structPtr, false);
//从内存空间拷到byte数组
Marshal.Copy(structPtr, bytes, 0, size);
//释放内存空间
Marshal.FreeHGlobal(structPtr);
//返回byte数组
return bytes;
}
/// <summary>
/// byte数组转结构体
/// </summary>
/// <param name="bytes">byte数组</param>
/// <param name="type">结构体类型</param>
/// <returns>转换后的结构体</returns>
public static object BytesToStuct(byte[] bytes, Type type)
{
//获得结构体的大小
int size = Marshal.SizeOf(type);
//byte数组长度小于结构体的大小
if (size > bytes.Length)
{
//返回空
return null;
}
//分配结构体大小的内存空间
IntPtr structPtr = Marshal.AllocHGlobal(size);
try
{
//将byte数组拷到分配好的内存空间
Marshal.Copy(bytes, 0, structPtr, size);
//将内存空间转换为目标结构体
return Marshal.PtrToStructure(structPtr, type);
}
finally
{
//释放内存空间
Marshal.FreeHGlobal(structPtr);
}
}
}
5.New出来的结构体是存在堆中仍是栈中?
有同事说new出来的都会放在堆里,我半信半疑。怎么去肯定,new出来的结构体到底放在哪里有两种方式,一种是使用Visual Studio的调试工具查看,这种方法找了很久没找到怎么去查看,路过的高手烦请指点下;第二种方法就是查看反编译dll的IL(Intermediate Language)语言。查看最终是以怎样的方式去实现的。不懂IL想了解IL的能够看此篇文章学习
5.1.不带形参的结构体构造
调用代码测试
//初始化结构体
TcpHeartPacket tcpHeartPacket = new TcpHeartPacket();
//将上报的心跳报文ReceviveBuff利用结构体静态帮助类StructHelper的BytesToStuct方法将字节流转化成结构体
tcpHeartPacket = (TcpHeartPacket)StructHelper.BytesToStuct(ReceviveBuff, tcpHeartPacket.GetType());
从对应的IL代码能够看出只是initobj,并无newobj,其中newobj表示分配内存,完成对象初始化;而initobj表示对值类型的初始化。
newobj用于分配和初始化对象;而initobj用于初始化值类型。所以,能够说,newobj在堆中分配内存,并完成初始化;而initobj则是对栈上已经分配好的内存,进行初始化便可,所以值类型在编译期已经在栈上分配好了内存。
newobj在初始化过程当中会调用构造函数;而initobj不会调用构造函数,而是直接对实例置空。
newobj有内存分配的过程;而initobj则只完成数据初始化操做。
initobj 的执行结果是,将tcpHeartPacket中的引用类型初时化为null,而基元类型则置为0。
综上,new 结构体(无参状况)是放在栈中的,只是作了null/0初始化。
5.2.带形参的结构体构造
接下来看下带形参的结构体存放位置。
简化版带形参的结构体以下:
public struct TcpHeartPacket
{
public TcpHeartPacket(byte _type)
{
type = _type;
}
public byte type;
}
调用以下:
//带形参结构体new初始化
TcpHeartPacket tcpHeartPacket = new TcpHeartPacket(0x1);
//类的new作对比
IWorkThread __workThread = new WorkThread();
IL代码以下:
造成了鲜明的对比,new带参的结构体。IL只是去call(调用)ctor(结构体的构造函数),而下面的new类则直接就是newobj,实例化了一个对象存到堆空间去了。
综合5.1,5.2代表结构体的new确实是存在栈里的,而类的new是存在堆里的。
6.性能测试
测试结果以下:
使用结构体解析包须要几十个微妙,其实效率仍是不好的。我用类封装成包,解析了,只须要几个微妙,性能差5到10倍。
7.缘由分析
主要时间消耗在了BytesToStuct方法,代码详见4
心跳包里面用了不少byte[]字节数组,而字节数组自己须要在堆里开辟空间;
该方法进行了装箱拆箱操做;
分配内存在堆上,仍是在堆上进行了copy操做;
拆装箱的IL代码以下:
装箱使用的box指令,取消装箱是 unbox.any 指令
声明:发布此文是出于传递更多知识以供交流学习之目的。如有来源标注错误或侵犯了您的合法权益,请做者持权属证实与咱们联系,咱们将及时更正、删除,谢谢。
做者:JerryMouseLi
来源:https://www.cnblogs.com/JerryMouseLi/p/12606920.html
More:【微信公众号】 u3dnotes
本文分享自微信公众号 - Unity3D游戏开发精华教程干货(u3dnotes)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。