类与结构体性能对比测试——以封装网络心跳包为例

1.背景

接上篇文章深入浅出C#结构体——封装以太网心跳包的结构为例,使用结构体性能不佳,而且也说明了原因。本篇文章详细描述了以类来封装网络心跳包的优缺点,结果大大提升了解析性能。

2.用类来封装以太网心跳包的优缺点

2.1.优点

  • 可以在类里直接new byte[],即直接实例字节数组,然后写初始化方法或者构造函数中直接对传进来的缓存进行拷贝赋值;
  • 无需装箱拆箱;
  • 类属于引用类型,无需像结构体进行值拷贝,底层直接就是智能指针;
  • 智能指针指向同一片内存,省内存空间;
  • 可以在类里写很多方便的方法,这也就是面向对象,面向领域的基石,方便以后扩展;

2.2.缺点

  • 存在堆里,读取性能会比栈稍差(现在PC端的计算速度很快,基本可忽略不计);
  • 虽然类也属于GC的托管资源,但是GC什么时候进行自动回收不可控制,需要实现IDisposable接口,用完该类,手动对该类进行释放动作;

使用类的实际性能怎样,我们用测试数据说话,后面会放上与结构体测试的性能对比数据。

3.网络心跳包封装类

这里全部都命名成了字节数组,包括 public byte[] type=new byte[1];因为如果是byte type类型,我不知道如何去释放这一值类型,怕到时候引起内存泄露等问题。然后在构造函数里面将缓存buf拷贝到了类的各个属性中,就是这么简单。

    public class TcpHeartPacketClass: BaseDisposable
    {
        private bool _disposed; //表示是否已经被回收
        public TcpHeartPacketClass(byte[] buf)
        {
            Buffer.BlockCopy(buf, 0, head, 0, 4);
            type[0] = buf[4];
            Buffer.BlockCopy(buf, 4, length, 0, 2);
            Buffer.BlockCopy(buf, 6, Mac, 0, 6);
            Buffer.BlockCopy(buf, 12, data, 0, 104);
            Buffer.BlockCopy(buf, 116, tail, 0, 4);
        }
        protected override void Dispose(bool disposing)
        {
            if (!_disposed) //如果还没有被回收
            {
                if (disposing) //如果需要回收一些托管资源
                {
                    //TODO:回收托管资源,调用IDisposable的Dispose()方法就可以
                    
                }
                //TODO:回收非托管资源,把之设置为null,等待CLR调用析构函数的时候回收
                head = null;
                type = null;
                length = null;
                Mac = null;
                data = null;
                tail = null;

                _disposed = true;

            }
            base.Dispose(disposing);//再调用父类的垃圾回收逻辑
        }

        public byte[] head=new byte[4];

        public byte[] type=new byte[1];

        public byte[] length = new byte[2];

        public byte[] Mac = new byte[6];

        public byte[] data = new byte[104];//数据体

        public byte[] tail = new byte[4];
    }  

4.实现IDisposable接口

用完类之后,为了主动去释放类,我封装了一个释放基类BaseDisposable。详见代码注释,有不明白的地方可以在评论区提问,我会详细作答。

    public class BaseDisposable : IDisposable
    {
        ~BaseDisposable()
        {
            //垃圾回收器将调用该方法,因此参数需要为false。
            Dispose(false);
        }

        /// <summary>
        /// 是否已经调用了 Dispose(bool disposing)方法。
        ///     应该定义成 private 的,这样可以使基类和子类互不影响。
        /// </summary>
        private bool disposed = false;

        /// <summary>
        /// 所有回收工作都由该方法完成。
        ///     子类应重写(override)该方法。
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            // 避免重复调用 Dispose 。
            if (!disposed) return;

            // 适应多线程环境,避免产生线程错误。
            lock (this)
            {
                if (disposing)
                {
                    // ------------------------------------------------
                    // 在此处写释放托管资源的代码
                    // (1) 有 Dispose() 方法的,调用其 Dispose() 方法。
                    // (2) 没有 Dispose() 方法的,将其设为 null。
                    // 例如:
                    //     xxDataTable.Dispose();
                    //     xxDataAdapter.Dispose();
                    //     xxString = null;
                    // ------------------------------------------------
                }

                // ------------------------------------------------
                // 在此处写释放非托管资源
                // 例如:
                //     文件句柄等
                // ------------------------------------------------
                disposed = true;
            }
        }

        /// <summary>
        /// 该方法由程序调用,在调用该方法之后对象将被终结。
        ///     该方法定义在IDisposable接口中。
        /// </summary>
        public void Dispose()
        {
            //因为是由程序调用该方法的,因此参数为true。
            Dispose(true);
            //因为我们不希望垃圾回收器再次终结对象,因此需要从终结列表中去除该对象。
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 调用 Dispose() 方法,回收资源。
        /// </summary>
        public void Close()
        {
            Dispose();
        }
    }

5.应用层调用

    DateTime packetClassStart = DateTime.Now;

    TcpHeartPacketClass tcpHeartPacketClass = neTcpHeartPacketClass(ReceviveBuff);

    DateTime packetClassEnd = DateTime.Now;
    TimeSpan toClassTs = packetClassEnd.Subtra(packetClassStart);
    try
    {
    tcpHeartPacketClass.head[0] = 0x11;
    
    LoggerHelper.Info("类中的包头:" + BitConverteToString(tcpHeartPacketClass.head));
    Console.WriteLine("类中的包头:{0}", BitConverteToString(tcpHeartPacketClass.head));

    LoggerHelper.Info("类中的包类型:" tcpHeartPacketClass.type.ToString());
    Console.WriteLine("类中的包类型:{0}"tcpHeartPacketClass.type.ToString());

    LoggerHelper.Info("类中的包长度:" + BitConverteToString(tcpHeartPacketClass.length));
    Console.WriteLine("类中的包长度:{0}", BitConverteToString(tcpHeartPacketClass.length));

    LoggerHelper.Info("类中的MAC地址:" + BitConverteToString(tcpHeartPacketClass.Mac));
    Console.WriteLine("类中的MAC地址:{0}", BitConverteToString(tcpHeartPacketClass.Mac));

    LoggerHelper.Info("类中的注册包内容:" + BitConverteToString(tcpHeartPacketClass.data));
    Console.WriteLine("类中的注册包内容:{0}"BitConverter.ToString(tcpHeartPacketClass.data));

    LoggerHelper.Info("类中的包尾:" + BitConverteToString(tcpHeartPacketClass.tail));
    Console.WriteLine("类中的包尾:{0}", BitConverteToString(tcpHeartPacketClass.tail));

    Console.WriteLine("字节数组类中分割总共花费{0}ms\n"toClassTs.TotalMilliseconds);
    }
    finally
    {
        IDisposable disposable = tcpHeartPacketClass as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }

6.Dispose()方法生效的测试

在ty...finally块执行完Dispose()方法之后,再去给类的某个属性赋值,我们看是否报错,如果报错赋值给空对象则证明释放成功。

    finally
    {
        IDisposable disposable = tcpHeartPacketClass        IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
    tcpHeartPacketClass.head[0] = 0x12;

如下报错,翻译过来意思就是对象引用没有对应的实例,也就是被我们给释放掉了。

7.测试性能对比

通过上图可以看到,上面的类解析的是微秒级别的,而文章深入浅出C#结构体——封装以太网心跳包的结构为例解析的是几十微秒级别的,差了差不多5到10倍的性能。

由此可见,在这种应用场景下,使用类来封装网络心跳包比结构体封装更合理。

8.综上,在C#里,结构体主要作用有如下两点:

  • 数据长度很短,构造16字节以下的新类型,而且结构体内的子类型必须是值类型,不然没意义,其目的是为了适应栈上的高效读取;
  • 为了兼容一些来自C跟C++的库;
    避开以上两点,我认为在C#新开发的应用程序中,可以完全的用类来取代结构体(仅代表个人观点)。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://www.cnblogs.com/JerryMouseLi/p/12610332.html

posted @ 2020-04-01 08:56  JerryMouseLi  阅读(...)  评论(...编辑  收藏