一种快速序列化技术






4.75/5 (44投票s)
2006年5月19日
4分钟阅读

231338

2988
透明地提升序列化性能并减小序列化对象的大小。
引言
在 .NET 中,序列化无处不在。您传递给远程对象、Web 服务或 WCF 服务的每个参数,或者从它们那里接收到的每个参数,都会在一端进行序列化,在另一端进行反序列化。那么,为什么还要写关于快速序列化的文章呢?肯定的是,标准的 BinaryFormatter
和 SoapFormatter
已经足够快了吧?
嗯,并不是。当使用 Remoting 在不同进程之间传递一个相当大的对象时,我们发现性能最高只能达到每秒 300 个调用。经过调查发现,每次序列化/反序列化周期需要 360 微秒,这本来没什么,但每秒 300 个调用意味着仅序列化就消耗了 11% 的 CPU!
背景
某种形式的自定义序列化是一个可行的选择。一个对象确切地知道它要序列化哪些字段以及它们的类型。它不需要所有通用开销和反射来弄清楚这一点并提取数据——它可以自己完成所有这些,效率要高得多。结果通常也更紧凑。在 .Shoaib 的文章 中有一个例子,展示了这些好处。
自定义序列化的缺点是接口不同,需要更改调用代码。而且,它也无法帮助 .NET 远程访问机制中的自动化序列化,除非您手动序列化到字节数组,然后将其作为参数传递。这不太符合类型安全!
下面我将介绍一种简单的方法,可以在保留自定义序列化优点的同时,保持标准的序列化接口以及由此带来的所有好处。
使用代码
正如在复杂的序列化问题中经常遇到的情况一样,解决方案在于实现 ISerializable
接口(有关入门知识,请参见 此处)。以下是我们使用的对象的简化版本
[Serializable]
public class TestObject : ISerializable {
public long id1;
public long id2;
public long id3;
public string s1;
public string s2;
public string s3;
public string s4;
public DateTime dt1;
public DateTime dt2;
public bool b1;
public bool b2;
public bool b3;
public byte e1;
public IDictionary<string,object> d1;
}
为了序列化一个对象,ISerializable
要求我们实现 GetObjectData
来定义要序列化的数据集合。这里的技巧是使用自定义序列化将所有字段合并到一个缓冲区中,然后将此缓冲区添加到 SerializationInfo
参数中,由标准格式化程序进行序列化。实现方式如下:
// Serialize the object. Write each field to the SerializationWriter
// then add this to the SerializationInfo parameter
public void GetObjectData (SerializationInfo info, StreamingContext ctxt) {
SerializationWriter sw = SerializationWriter.GetWriter ();
sw.Write (id1);
sw.Write (id2);
sw.Write (id3);
sw.Write (s1);
sw.Write (s2);
sw.Write (s3);
sw.Write (s4);
sw.Write (dt1);
sw.Write (dt2);
sw.Write (b1);
sw.Write (b2);
sw.Write (b3);
sw.Write (e1);
sw.Write<string,object> (d1);
sw.AddToInfo (info);
}
SerializationWriter
类扩展了 BinaryWriter
,以增加对附加数据类型(DateTime
和 Dictionary
)的支持,并简化 SerializationInfo
的接口。它还重写了 BinaryWriter
的 Write(string)
方法,以支持 null 字符串。这里我不详细介绍实现细节。代码中为感兴趣的读者提供了大量的解释。
ISerializable
还要求我们定义一个构造函数来将流反序列化为新对象。这里的过程与上述一样简单
// Deserialization constructor. Create a SerializationReader from
// the SerializationInfo then extract each field from it in turn.
public TestObject (SerializationInfo info, StreamingContext ctxt) {
SerializationReader sr = SerializationReader.GetReader (info);
id1 = sr.ReadInt64 ();
id2 = sr.ReadInt64 ();
id3 = sr.ReadInt64 ();
s1 = sr.ReadString ();
s2 = sr.ReadString ();
s3 = sr.ReadString ();
s4 = sr.ReadString ();
dt1 = sr.ReadDateTime ();
dt2 = sr.ReadDateTime ();
b1 = sr.ReadBoolean ();
b2 = sr.ReadBoolean ();
b3 = sr.ReadBoolean ();
e1 = sr.ReadByte ();
d1 = sr.ReadDictionary<string,object> ();
}
同样,SerializationReader
扩展了 BinaryReader
,原因与上面相同。
随着时间的推移,我可能会扩展写入器和读取器可以高效处理的类型集合。已经提供了 WriteObject()
和 ReadObject()
方法,它们可以写入任何任意类型,但这只是回退到标准的二进制序列化(除非它是受支持的快速类型之一)。
结果
下载中包含的测试程序只是创建并填充 TestObject
,并以微秒/周期的粒度测量其序列化和反序列化时间,平均 250,000 个周期。所有计时都在一台 1.5GHz Pentium M 笔记本电脑上进行。结果如下:
Formatter | 尺寸(字节) | 时间(微秒) | |
---|---|---|---|
标准序列化 | 二进制 | 2080 | 364 |
快速序列化 | 二进制 | 421 | 74 |
快速序列化 | SOAP | 1086 | 308 |
因此,下面介绍的快速序列化技术可以将大小和序列化/反序列化时间缩短到标准序列化的大约五分之一。即使是 SOAP 序列化(通常比二进制慢 2 到 3 倍),也比标准的二进制序列化更快。
摘要
通过这种方式将自定义序列化与 ISerializable
相结合,可以在不更改相关对象的处理方式的情况下,带来显著的性能提升。它允许在已识别出性能问题的特定对象上透明地添加快速序列化。
在我们的实际案例中,仅通过更改一个关键对象的这个设置,吞吐量就从每秒 300 个 Remoting 调用提高到 700 多个。没有进行其他任何更改。
还有一个意想不到的好处。您会注意到上面没有 SoapFormatter
的比较数据,这是因为 MS 没有为 SoapFormatter
提供处理泛型类型的功能。使用上述技术意味着 SoapFormatter
永远不会看到被自定义序列化为字节数组的泛型类型,因此这个限制被消除了。
将自定义序列化与 ISerializable
相结合,永远不可能像纯自定义序列化那样快。然而,它仍然属于标准序列化框架的优点,使其成为一种有用的技术,可以在不影响其他代码的情况下提高性能。
历史
- 初版 - 2005 年 5 月 19 日。
这是我在 CodeProject 的第一个帖子——请手下留情!