BinaryFormatter 与手动序列化
比较使用 BinaryFormatter(标准的 .NET 类)进行序列化与手动逐字节序列化;在选择适合您的方法之前,一些优缺点。
引言
首先,让我们澄清“序列化”一词
在计算机科学中,在数据存储和传输的上下文中,序列化是将数据结构或对象状态转换为可以存储(例如,在文件或内存缓冲区中,或通过网络连接链路传输)并在同一或另一个计算机环境中稍后“恢复”的格式的过程。(维基百科)
序列化是将对象的状态转换为可以持久化或传输的形式的过程。序列化的补充是反序列化,它将流转换为对象。(MSDN)
序列化技术大致可分为两种:**人类可读**和**二进制**。 .NET Framework 提供了 BinaryFormatter
类用于二进制序列化,并提供了 XmlSerializer
类用于人类可读技术。
大多数来源建议,如果我们需要的序列化数据量较小,则使用 BinaryFormatter
而不是 XmlSerializer
。我几乎从不使用 XmlSerializer
- 它会对您的类带来一些限制。然而,BinaryFormatter
的使用相当频繁,我对此有一些看法。
背景
我在 .NET Framework 2.0 发布时开始使用 BinaryFormatter
。当时还没有 WCF,二进制序列化被 Remoting 使用。我们开发了一个应用程序,其中应该有一个带有消息系统的特殊协议。我们有权选择消息编码方式。我们决定尝试 BinaryFormatter
,它运行正常。一段时间后。当我们尝试在循环中发送一千条消息时 - 我们遇到了 TCP 零窗口大小的问题 - 这意味着接收方无法及时反序列化消息。
当我检查网络流量时,我发现每条消息的大小约为 1000 字节,而手动逐字节编码最多只需要 70 字节,这让我非常惊讶。
BinaryFormatter 和手动方法
在示例应用程序中,我们有两个“有用”的类 - Person
聚合 CreditCard
,如图所示
我们将序列化 Person
类的对象。要使用 BinaryFormatter
进行序列化,我们需要在 Person
类定义之前,以及在 Person
聚合的所有类之前,添加 Serializable
属性。然后,我们可以编写
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, toSerialize);
其中 stream
和 toSerialize
分别是 Stream
和 Person
类型的变量。
实现手动同步时,事情会变得有点复杂。我们必须提供一些方法供以后调用,而不是放置 Serializable
属性。在示例中,这样的方法是序列化类内部的静态成员 WriteToStream
和 ReadFromStream
。然后我们写
Person.WriteToStream(toSerialize, stream);
就是这样。
使用代码
在简要探讨了这两种技术后,让我们来检查测试代码。
static byte[] getBytesWithBinaryFormatter(Person toSerialize)
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, toSerialize);
return stream.ToArray();
}
static byte[] getBytesWithManualWrite(Person toSerialize)
{
MemoryStream stream = new MemoryStream();
Person.WriteToStream(toSerialize, stream);
return stream.ToArray();
}
static void testSize(Person toTest)
{
byte[] fromBinaryFormatter = getBytesWithBinaryFormatter(toTest);
Console.WriteLine("\tWith binary formatter: {0} bytes",
fromBinaryFormatter.Length);
byte[] fromManualWrite = getBytesWithManualWrite(toTest);
Console.WriteLine("\tManual serializing: {0} bytes", fromManualWrite.Length);
}
static void testTime(Person toTest, int timesToTest)
{
List<byte[]> binaryFormatterResultArrays = new List<byte[]>();
List<byte[]> manualWritingResultArrays = new List<byte[]>();
DateTime start = DateTime.Now;
for (int i = 0; i < timesToTest; ++i )
binaryFormatterResultArrays.Add(getBytesWithBinaryFormatter(toTest));
TimeSpan binaryFormatterSerializingTime = DateTime.Now - start;
Console.WriteLine("\tBinary formatter serializing: {0} seconds",
binaryFormatterSerializingTime.TotalSeconds);
start = DateTime.Now;
for (int i = 0; i < timesToTest; i++)
manualWritingResultArrays.Add(getBytesWithManualWrite(toTest) );
TimeSpan manualSerializingTime = DateTime.Now - start;
Console.WriteLine("\tManual serializing: {0} seconds",
manualSerializingTime.TotalSeconds);
BinaryFormatter formatter = new BinaryFormatter();
start = DateTime.Now;
foreach (var next in binaryFormatterResultArrays)
formatter.Deserialize(new MemoryStream(next));
TimeSpan binaryFormatterDeserializingTime = DateTime.Now - start;
Console.WriteLine("\tBinaryFormatter desrializing: {0} seconds",
binaryFormatterDeserializingTime.TotalSeconds );
start = DateTime.Now;
foreach (var next in manualWritingResultArrays)
Person.ReadFromStream(new MemoryStream(next));
TimeSpan manualDeserializingTime = DateTime.Now - start;
Console.WriteLine("\tManual desrializing: {0} seconds",
manualDeserializingTime.TotalSeconds );
}
static void addCards(Person where, int howMany)
{
for (int i = 0; i < howMany; ++i)
{
where.AddNewCard(new CreditCard(Guid.NewGuid().ToString(),
new DateTime(2012, 12, 24 )));
}
}
static void Main(string[] args)
{
Person aPerson = new Person("Kate", new DateTime(1986, 2, 1),
new CreditCard[] { new CreditCard(Guid.NewGuid().ToString(),
new DateTime(2012, 12, 24 )) });
Console.WriteLine("Size test: 1 card");
testSize(aPerson);
Console.WriteLine("Time test: 1 card, 100 000 times");
testTime(aPerson, 100000);
addCards(aPerson, 10000);
Console.WriteLine("Size test: 10 000 cards");
testSize(aPerson);
Console.WriteLine("Time test: 10 000 cards, 100 times");
testTime(aPerson, 100);
Console.ReadKey();
}
首先,对包含一个 CreditCard
对象的 Person
进行大小和时间测试。然后对一个拥有 10,000 张信用卡的人进行相同的测试 :)。以下是我机器的输出
Size test: 1 card
With binary formatter: 896 bytes
Manual serializing: 62 bytes
Time test: 1 card, 100 000 times
Binary formatter serializing: 4,24 seconds
Manual serializing: 0,28 seconds
BinaryFormatter desrializing: 4,57 seconds
Manual desrializing: 0,169 seconds
Size test: 10 000 cards
With binary formatter: 640899 bytes
Manual serializing: 450062 bytes
Time test: 10 000 cards, 100 times
Binary formatter serializing: 4,713 seconds
Manual serializing: 0,527 seconds
BinaryFormatter desrializing: 4,519 seconds
Manual desrializing: 0,57 seconds
基于这些结果以及我们所掌握的知识,让我们比较一下这两种技术。
大小
使用 BinaryFormatter
序列化消息的成本很高,因为它包含元数据。手动序列化在此提供了实际的好处。在示例中,我们获得的字节数组结果缩小了近 14 倍。但是,当我们谈论序列化对象数组时 - 那么结果数组的大小差异就没有那么大了 - 在我们的示例中,手动同步的结果数组要小 1.4 倍。
速度
在速度方面,我们为单个对象和对象数组的序列化都获得了不错的优势。对于一个 Person
和一个 CreditCard
对象,我们获得了 4.24 / 0.28 = 15.14 倍的速度提升。并且手动方法对于 Person
对象内的 10,000 个 CreditCard
对象来说,速度快了近 10 倍。
速度测试通常需要提及机器的参数,一个显示一些依赖关系的表格 - 欢迎您自己测试并绘制表格。我不是试图给出精确的结果,而是呈现一个普遍的图景。
版本控制
好吧,协议会改变,三年后,您的初始消息格式可能与现在大不相同。字段可能会被删除和添加。因为我们在网络环境中考虑序列化,所以我们应该记住:所有对等方都应该对消息格式的变化做出良好的响应。
在手动序列化方法中,我们有两种选择:要么在格式更改时更新所有对等方,要么提供自己的方法来处理接收到其他版本消息的情况。
在 .NET 中,新字段可以标记为可选 - 它们可能存在于消息中 - 也可能不存在。当然,这并不能解决所有问题,并且新版本的宿主在接收消息时,应该处理可选字段的存在和缺失。
开发速度
手动序列化确实需要时间。用于开发和测试。如果您有版本控制逻辑 - 那么它将花费更多的时间。标准的 .NET 方法为您节省了所有这些时间。
技术
当您使用 TCP 绑定时,WCF 中会使用 BinaryFormatter
。理论上,您可以提供自己的格式化程序,但我从未听说过任何好的实现。
结构复杂度影响
BinaryFormatter
能够序列化任何复杂度的对象。这意味着,即使您的系统中的对象形成了一个带有循环的图 - 它也会被正确序列化。同样,序列化(以及反序列化!)类层次结构中的叶类也不是问题。
手动序列化方面情况就没那么乐观了。拥有一个不聚合任何其他类的类是微不足道的情况。然后,当您有一个对象树时(Person
内部有 100 个 CreditCard
对象就是一个树)- 我们会遇到一些复杂性。拥有类层次结构会带来更多复杂性,而循环的独立性会让您的代码完全难以理解。我个人从未对可能包含循环的对象图使用过手动同步。
摘要
BinaryFormatter
的**优点**
- 能够使用 WCF 或其他技术
- 无需花费时间实现序列化机制
- 存在版本控制支持
- 可以处理任何结构复杂度的对象
手动序列化的**优点**
- 可以达到最佳的大小复杂度
- 可以达到最佳性能
关注点
手动序列化:叶类序列化
序列化,特别是反序列化代码,会涉及许多方面。
手动序列化:序列化代码放在哪里?
实际上,这是之前关注点的一个延续。在这个示例中,我将代码作为静态方法放在各自的类中。这种方法可以通过信息专家 GRASP 模式来解释。但是,由于没有类层次结构,示例项目中的一切都显得整洁。当出现类层次结构时,使用静态方法的方法看起来很糟糕。也许这一点可以在下一篇文章中涵盖。
Protobuff:http://protobuf-net.googlecode.com。有趣的项目,非常辛苦的工作。我对他们的序列化器进行了相同的测试 - 并且获得了与手动方法相同的尺寸特性!然而,时间消耗比 BinaryFormatter
更糟糕 **(在 Debug 生成中 - 感谢 Sam Cragg 的评论)**。您可以 下载此代码 - 92.5 KB - 与示例项目相同,但还包含 Protobuff 检查,以便您自己查看。Release 生成的 Protobuff-net 测试 在此 - 86.6 KB。Release 生成显示出比 BinaryFormatter
好 3-5 倍的时间特性,但比手动方法的时间特性差。
为什么 BinaryFormatter 如此缓慢且占用空间?
历史
- 2012 年 10 月 1 日 - 首次上传。
- 2012 年 1 月 13 日 - 更新以反映反馈中的评论。