C#高性能序列化、超高性能、超轻量级二进制序列化


概述

本猿在前面两篇博客(分析C# 二进制序列化诟病所在,并解决问题(一)分析C# 二进制序列化诟病所在,并解决问题(二))将C#自带的二进制序列化已经基本上优化到了极致,但是系统自带的二进制还是有很多让人不舒服的点,比如Serializable标签强制强类型序列化数据庞大等,而这一切所带来的就是性能问题,那么我们这节就来场极致的优化吧。

交流方式

性能测试

为了节约大家时间,先展示性能测试,当然了,一切不说硬件的测试都是耍流氓,那么先康康本猿的新助手配置。

测试硬件:戴尔笔记本G5,i7-10750H,8G RAM+256G固态+1T机械。
测试系统:Window10 家庭版,x64位。

测试内容:
待测试类

 [Serializable]
    public class Student
    {
        public int P1 { get; set; }
        public string P2 { get; set; }
        public long P3 { get; set; }
        public byte P4 { get; set; }
        public DateTime P5 { get; set; }
        public decimal P6 { get; set; }

        public List<int> List1 { get; set; }
        public List<string> List2 { get; set; }
        public List<byte[]> List3 { get; set; }
       
        public Dictionary<int, int> Dic1 { get; set; }
        public Dictionary<int, string> Dic2 { get; set; }
        public Dictionary<string, string> Dic3 { get; set; }
        public Dictionary<int, Arg> Dic4 { get; set; }
    }
    [Serializable]
    public class Arg
    {
        public Arg(int myProperty)
        {
            this.MyProperty = myProperty;
        } 
        
        public Arg()
        {
           
        }
        
        public int MyProperty { get; set; }
    }

类初始化

 Student student = new Student();
            student.P1 = 10;
            student.P2 = "若汝棋茗";
            student.P3 = long.MaxValue;
            student.P4 = 0;
            student.P5 = DateTime.Now;
            student.P6 = 10;
            student.P7 = new byte[1024 * 64];

            Random random = new Random();
            random.NextBytes(student.P7);

            student.List1 = new List<int>();
            student.List1.Add(1);
            student.List1.Add(2);
            student.List1.Add(3);

            student.List2 = new List<string>();
            student.List2.Add("1");
            student.List2.Add("2");
            student.List2.Add("3");

            student.List3 = new List<byte[]>();
            student.List3.Add(new byte[1024]);
            student.List3.Add(new byte[1024]);
            student.List3.Add(new byte[1024]);

            student.Dic1 = new Dictionary<int, int>();
            student.Dic1.Add(1, 1);
            student.Dic1.Add(2, 2);
            student.Dic1.Add(3, 3);

            student.Dic2 = new Dictionary<int, string>();
            student.Dic2.Add(1, "1");
            student.Dic2.Add(2, "2");
            student.Dic2.Add(3, "3");

            student.Dic3 = new Dictionary<string, string>();
            student.Dic3.Add("1", "1");
            student.Dic3.Add("2", "2");
            student.Dic3.Add("3", "3");

            student.Dic4 = new Dictionary<int, Arg>();
            student.Dic4.Add(1, new Arg(1));
            student.Dic4.Add(2, new Arg(2));
            student.Dic4.Add(3, new Arg(3));

测试方法:
循环序列化100万次。TimeMeasurer.Run的方法是RRQMCore的一个测试代码片运行时间的方法,实质就是StopWatch。

TimeSpan timeSpan = RRQMCore.Diagnostics.TimeMeasurer.Run(() =>
              {
                  for (int i = 0; i < 1000000; i++)
                  {
                      byte[] datas = SerializeConvert.BinarySerialize(student);
                      if (i % 10000 == 0)
                      {
                          Console.WriteLine(i);
                      }
                  }
              });

            Console.WriteLine(timeSpan);

结果:

系统普通二进制序列化

耗时:2分06秒
内存:上升到17Mb
GC:极度频繁

在这里插入图片描述

经过优化的系统二进制序列化

耗时:1分36秒
内存:上升到16Mb
GC:中度释放

在这里插入图片描述
超轻量二进制序列化

耗时:22秒
内存:上升到16Mb
GC:中度释放

在这里插入图片描述
从上图可以看出优化结果是非常nice的,那么就让我们开始吧。

分析

序列化的实质就是将特殊的数据结构转换为二进制流,在编程语言中也就是byte数组。所以我们只需要找寻一种方法,能够将一个数据结果的值取出来,然后再能还回去即可。

设计思路

因为要超轻量,就是那种比Json还轻量的那种,所以在序列化后的数据中只包含值,连属性名都不包含,那这样的设计就比较有局限性,具体表现如下:

  1. 不支持接口、抽象类
  2. 不支持具有里氏转换操作的类(子类赋值给父类)
  3. 支持不同类序列化,但是必须属性及属性顺序均相同。
  4. 支持字典、数组、列表

例如下列数据结构:

public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

在初始化后

   Person person = new Person();
          person.Name = "张三";
          person.Age = 18;

Person拥有两个属性值,所以我们只需要按顺序记住“张三”和“18”,但是还需要记住每个属性的长度及整个类的长度,长度的记录采用4个byte转换成int的值表示。如下:

  • 橘黄色块中的4表示4个byte,用来记录整个类的长度
  • 棕色块区中,4个byte表示“张三”这个字符串的长度
  • 绿色块区中,4个byte表示“18”这个数字的长度

在这里插入图片描述
在明白了上述设计后,康康代码。

序列化基础类型

 private int SerializeObject(Stream stream, object graph)
        {
            int len = 0;
            byte[] data = null;

            long position = stream.Position;

            if (graph != null)
            {
                if (graph is string str)
                {
                    data = Encoding.UTF8.GetBytes(str);
                }
                else if (graph is byte by)
                {
                    data = new byte[] { by };
                }
                else if (graph is bool b)
                {
                    data = BitConverter.GetBytes(b);
                }
                else if (graph is short s)
                {
                    data = BitConverter.GetBytes(s);
                }
                else if (graph is int)
                {
                    data = BitConverter.GetBytes((int)graph);
                }
                else if (graph is long l)
                {
                    data = BitConverter.GetBytes(l);
                }
                else if (graph is float f)
                {
                    data = BitConverter.GetBytes(f);
                }
                else if (graph is double d)
                {
                    data = BitConverter.GetBytes(d);
                }
                else if (graph is DateTime time)
                {
                    data = Encoding.UTF8.GetBytes(time.Ticks.ToString());
                }
                else if (graph is Enum)
                {
                    var enumValType = Enum.GetUnderlyingType(graph.GetType());

                    if (enumValType == byteType)
                    {
                        data = new byte[] { (byte)graph };
                    }
                    else if (enumValType == shortType)
                    {
                        data = BitConverter.GetBytes((short)graph);
                    }
                    else if (enumValType == intType)
                    {
                        data = BitConverter.GetBytes((int)graph);
                    }
                    else
                    {
                        data = BitConverter.GetBytes((long)graph);
                    }
                }
                else if (graph is byte[])
                {
                    data = (byte[])graph;
                }
                else
                {
                    stream.Position += 4;
                    Type type = graph.GetType();

                    if (typeof(IEnumerable).IsAssignableFrom(type))
                    {
                        len += SerializeIEnumerable(stream, (IEnumerable)graph);
                    }
                    else
                    {
                        len += SerializeClass(stream, graph, type);
                    }
                }
            }

            long oldPosition;
            if (data != null)
            {
                len = data.Length;
                oldPosition = len + position+4;
            }
            else
            {
               oldPosition  = stream.Position;
            }
           
            byte[] lenBuffer = BitConverter.GetBytes(len);
            stream.Position = position;
            stream.Write(lenBuffer, 0, lenBuffer.Length);

            if (data != null)
            {
                stream.Write(data, 0, data.Length);
            }
            stream.Position = oldPosition;
            return len + 4;
        }

当序列化对象为非基本类型时,遍历出所有的属性,然后再继续。

 PropertyInfo[] propertyInfos = this.GetProperties(type);
                foreach (PropertyInfo property in propertyInfos)
                {
                    len += SerializeObject(stream, property.GetValue(obj, null));
                }

相信看到这里,大家已经对序列化有了一个简单的认识了,那么接下来再康康反序列化。

反序列化之所以不支持接口、抽象类等的原因就是在反序列化时,必须通过目标类型创建空对象。例如对于Person类,就可以先生成一个Person的对象,然后再逐属性赋值,这也就是为什么属性顺序不能乱的原因了。


                InstanceObject typeInfo = InstanceCache.GetOrAdd(type.FullName, (v) =>
                {
                    InstanceObject instanceObject = new InstanceObject();
                    instanceObject.Type = type;
                    instanceObject.Properties = this.GetProperties(type);
                    instanceObject.ProTypes = instanceObject.Properties.Select(a => a.PropertyType).ToArray();

                    if (type.IsGenericType)
                    {
                        instanceObject.AddMethod = type.GetMethod("Add");
                        instanceObject.ToArrayMethod = type.GetMethod("ToArray");

                        instanceObject.ArgTypes = type.GetGenericArguments();
                        type = type.GetGenericTypeDefinition().MakeGenericType(instanceObject.ArgTypes);

                        if (instanceObject.ArgTypes.Length == 1)
                        {
                            instanceObject.instanceType = InstanceType.List;
                        }
                        else
                        {
                            instanceObject.instanceType = InstanceType.Dictionary;
                        }
                    }
                    else
                    {
                        instanceObject.instanceType = InstanceType.Class;
                    }
                    return instanceObject;
                });
                typeInfo.Instance = Activator.CreateInstance(type);

结束

OK了,超高性能序列化就讲到这里,文中的代码只是一部分,因为源代码也比较多,所以不能全部贴出来。而且该代码已经封装在RRQMCore中,点击下载源代码。
当然如果只想使用的猿友还可以通过Nuget下载RRQMCore包,直接使用。
在这里插入图片描述
在这里插入图片描述
安装以后直接使用RRQMBinaryFormatter类,使用方法和系统自带的BinaryFormatter基本一致。

最后希望这篇博客能帮到各位猿友,当然如果还有什么不明白的,可以私信本猿哦,QQ群:234762506

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C# 中,可以使用 BinaryFormatter 类来进行二进制序列化和反序列化操作。下面是一个简单的示例: ```csharp using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; [Serializable] public class Person { public string Name { get; set; } public int Age { get; set; } } public class Program { static void Main(string[] args) { // 创建一个 Person 对象 Person person = new Person { Name = "Tom", Age = 18 }; // 创建一个 BinaryFormatter 对象 BinaryFormatter formatter = new BinaryFormatter(); // 将 Person 对象序列化二进制流 MemoryStream stream = new MemoryStream(); formatter.Serialize(stream, person); // 将二进制流反序列化为 Person 对象 stream.Seek(0, SeekOrigin.Begin); Person newPerson = (Person)formatter.Deserialize(stream); // 输出序列化后的 Person 对象的属性值 Console.WriteLine($"Name:{newPerson.Name}, Age:{newPerson.Age}"); } } ``` 在上面的示例中,我们首先创建了一个 Person 类,并将其标记为可序列化(即添加了 `[Serializable]` 属性)。然后,我们在 `Main` 方法中创建了一个 Person 对象,并使用 BinaryFormatter 对象将其序列化为一个 MemoryStream 对象。接着,我们将 MemoryStream 对象的读取位置设置为开头,并使用 BinaryFormatter 对象将其反序列化为一个新的 Person 对象。最后,我们输出新的 Person 对象的属性值。 需要注意的是,BinaryFormatter 类在进行序列化和反序列化时,会将对象的完整类型信息也序列化二进制流中。因此,在进行反序列化时,需要确保反序列化的对象类型与序列化时的对象类型完全一致,否则会抛出异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若汝棋茗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值