概述
本猿在前面两篇博客(分析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还轻量的那种,所以在序列化后的数据中只包含值,连属性名都不包含,那这样的设计就比较有局限性,具体表现如下:
- 不支持接口、抽象类
- 不支持具有里氏转换操作的类(子类赋值给父类)
- 支持不同类序列化,但是必须属性及
属性顺序
均相同。 - 支持字典、数组、列表
例如下列数据结构:
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