深入浅出C#结构体
1.应用背景
底端设备有大量网络报文(字节数组):心跳报文,数据采集报文,告警报文上报。需要有对应的报文结构去解析这些字节流数据。
2.结构体解析
由此,我第一点就想到了用结构体去解析。原因有以下两点:
2.1.结构体存在栈中
类属于引用类型,存在堆中;结构体属于值类型,存在栈中,在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。
2.2.结构体不需要手动释放
属于托管资源,系统自动管理生命周期,局部方法调用完会自动释放,全局方法会一直存在。
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 指令
8.下一篇:类与结构体性能对比测试——以封装网络心跳包为例
当数据比较大的时候,结构体这种数据复制机制会带来较大的开销。也难怪微软给出的准则中有一条:“当类型定义大于16字节时不要选用struct”。最终我也选择了类来封装以太网包的解析,性能可以达到微妙级,会在下一篇文章《类与结构体性能对比测试——以封装网络心跳包为例》中作详细描述。
9.IL工具使用分享
- 使用ildasm工具
VS2013外部工具中添加ildasm.exe - 使用dnSpy工具
dnSpy的github地址
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/JerryMouseLi/p/12606920.html
深入浅出C#结构体的更多相关文章
- 智能合约语言 Solidity 教程系列6 - 结构体与映射
写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所了解, 如果你还不了解,建议你先看以太坊是什么 本系列文章一部分是参考Solidity官方文档(当前最新版 ...
- Go结构体实现类似成员函数机制
Go语言结构体成员能否是函数,从而实现类似类的成员函数的机制呢?答案是肯定的. package main import "fmt" type stru struct { testf ...
- C#基础回顾(二)—页面值传递、重载与重写、类与结构体、装箱与拆箱
一.前言 -孤独的路上有梦想作伴,乘风破浪- 二.页面值传递 (1)C#各页面之间可以进行数据的交换和传递,页面之间可根据获取的数据,进行各自的操作(跳转.计算等操作).为了实现多种方式的数据传递,C ...
- go语言结构体
定义: 是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体. 成员: 每个值称为结构体的成员. 示例: 用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号.员工的名字. ...
- C语言中的结构体
用户自己建立自己的结构体类型 1. 定义和使用结构体变量 (1).结构体的定义 C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体. (2).声明一个结构体类型的一般形式为: ...
- C++_系列自学课程_第_12_课_结构体
#include <iostream> #include <string> using namespace std; struct CDAccount { double bal ...
- java socket传送一个结构体给用C++编写的服务器解析的问题
另一端是Java写客户端程序,两者之间需要通信.c++/c接收和发送的都是结构体,而Java是直接发送的字节流或者byte 数组.解决方法:c++/c socket 在发送结构体的时候其实发送的也是字 ...
- swift学习笔记3——类、结构体、枚举
之前学习swift时的个人笔记,根据github:the-swift-programming-language-in-chinese学习.总结,将重要的内容提取,加以理解后整理为学习笔记,方便以后查询 ...
- HDOJ 1009. Fat Mouse' Trade 贪心 结构体排序
FatMouse' Trade Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) ...
随机推荐
- Alberto Del Bimbo:为什么说研究员要有想象力?
Del Bimbo:为什么说研究员要有想象力?" title="Alberto Del Bimbo:为什么说研究员要有想象力?"> 说到科研,与日本式的&q ...
- 如何在自己的CSDN博客中增添【高大上】的博客栏目?
前几天看到过一位博主的博客界面,向下看 ☟ (博主对不起啊!把你的公众号给抹了~~~),感觉做这个东西挺好玩的,而且我竟然找不到在哪个地方可以设置!在百度上也没有搜到教程,最后问了一下贺老师知道了入口 ...
- jq拖拽插件
(function ($) { var move = false; //标记控件是否处于被拖动状态 var dragOffsetX = 0; //控件左边界和鼠标X轴的差 var dragOffset ...
- 50-Python2和3字符编码的区别
目录 Python2和3字符编码的区别 python2 python3 Python2和3字符编码的区别 区别点 python2 python3 print 是一个语法结构 是一个函数,print(' ...
- Docker实战之Zookeeper集群
1. 概述 这里是 Docker 实战系列第四篇.主要介绍分布式系统中的元老级组件 Zookeeper. ZooKeeper 是一个开源的分布式协调服务,是 Hadoop,HBase 和其他分布式框架 ...
- 使用IDEA创建Maven整合SSM
创建数据库 CREATE DATABASE `ssmbuild`; USE `ssmbuild`; DROP TABLE IF EXISTS `books`; CREATE TABLE `books` ...
- css布局中的各种FC(BFC、IFC、GFC、FFC)
什么是FC?FC(Formatting Context)格式化上下文,其实指的是一个渲染区域,拥有一套渲染规则,它决定了其子元素如何定位,以及与其他元素之间的关系和相互作用. 什么是BFC? BFC( ...
- 关于css 的垂直居中
对于元素的水平居中,我根据我自己之前的一些学习来进行一些总结,如果有不对的地方,欢迎指正~ 一.让大小不固定的元素垂直居中 因为:表格的单元格的特别属性:垂直居中等: `div.parent { di ...
- js中(event)事件对象
事件对象 • 什么是事件对象? • 就是当你触发了一个事件以后,对该事件的一些描述信息 • 例如: ° 你触发一个点击事件的时候,你点在哪个位置了,坐标是多少 ° 你触发一个键盘事件的时候,你按的是哪 ...
- .net core WebAPI+EF 动态接收前台json,并动态修改数据库
用API开发的人都知道,常用的后台接收参数就是建个DTO,然后前台把这个DTO传过来.后台再更新,例如如下例子: public async Task<IActionResult> PutM ...