65.9K
CodeProject 正在变化。 阅读更多。
Home

一种快速序列化技术

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (44投票s)

2006年5月19日

4分钟阅读

viewsIcon

231338

downloadIcon

2988

透明地提升序列化性能并减小序列化对象的大小。

引言

在 .NET 中,序列化无处不在。您传递给远程对象、Web 服务或 WCF 服务的每个参数,或者从它们那里接收到的每个参数,都会在一端进行序列化,在另一端进行反序列化。那么,为什么还要写关于快速序列化的文章呢?肯定的是,标准的 BinaryFormatterSoapFormatter 已经足够快了吧?

嗯,并不是。当使用 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,以增加对附加数据类型(DateTimeDictionary)的支持,并简化 SerializationInfo 的接口。它还重写了 BinaryWriterWrite(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 的第一个帖子——请手下留情!

© . All rights reserved.