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

优化 .NET 中的序列化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (86投票s)

2006年9月25日

公共领域

31分钟阅读

viewsIcon

684554

downloadIcon

5023

提供代码和技术,使开发人员能够优化数据序列化

引言

这是关于优化序列化(尤其用于远程处理)的两篇(可能三篇,取决于兴趣)文章中的第一篇。

第一篇文章包含通用代码,用于将“拥有的数据”(稍后定义)以最高速度存储在一个紧凑的单元中。第二篇文章将提供一个示例,说明如何使用此代码将数据集序列化为独立单元。可能第三篇文章将介绍如何将 LLBLGenPro(领先的 O/R 映射器)中的 EntityEntityCollection 序列化,以此为例说明如何接管整个序列化过程以获得出色的结果。尽管这特定于某个应用程序,但您可能会发现其中使用的技术在您的代码中有用。

本文中的代码灵感来自 CodeProject 上的三篇文章

背景

如果您曾经使用 .NET 远程处理大量数据,您会发现它存在可伸缩性问题。对于少量数据,它工作得足够好,但大量数据会消耗大量 CPU 和内存,生成大量用于传输的数据,并可能因内存不足异常而失败。序列化实际执行的时间也是一个大问题——大量数据可能使其在应用程序中不可行,无论 CPU/内存开销如何,仅仅因为它花费太长时间来进行序列化/反序列化。通过服务器和客户端接收器使用数据压缩可以帮助减小最终传输大小,但无助于解决过程中早期过多的开销。

话虽如此,您不能因此责怪 .NET,请记住它所做的所有工作:它将确保发现重建原始对象所需的整个对象图,并正确处理对同一对象的多个引用,以确保只反序列化一个公共实例。它还必须通过反射来实现这一点,并且能够在事先不知道涉及的对象的情况下做到这一点,所以总的来说,它做得相当好。它还将允许您通过实现 ISerializable 接口来参与序列化/反序列化过程,如果您知道自己可以比仅仅通过反射重新创建字段数据做得更好。

这里的关键在于“事先了解”。我们可以利用这一点来优化某些“拥有的数据”(稍后定义)的存储方式,并让 .NET 来处理其余部分。这正是本文将要讨论的内容。

作为预览,我将举一个可能的优化规模的例子

我有一组来自数据库表中 34,423 行的参考数据,它存储在一个实体集合中。序列化这组数据(到 MemoryStream 以获得最大速度)花费了惊人的 92 秒,并生成了 13.5MB 的序列化数据。反序列化这些数据大约需要 58 秒——在远程处理场景中不可用!

通过使用本文中的技术,我能够将相同的数据序列化到 2.1MB,序列化仅花费 0.35 秒,反序列化仅花费 0.82 秒!此外,CPU 使用率和内存仅为原始 .NET 序列化器使用量的一小部分。

Using the Code

如引言中所述,可下载的代码是通用的(必须避免使用“泛型”!),因此没有特别针对远程处理的内容。基本上,您将“拥有的”数据放入 SerializationWriter 类的实例中,然后使用 ToArray 方法获取包含序列化数据的 byte[]。然后,您可以像往常一样将其存储在传递给 ISerializable.GetObjectData() 方法的 SerializationInfo 参数中,如下所示:

public virtual void GetObjectData(SerializationInfo info,
                                  StreamingContext context)
{
    SerializationWriter writer = new SerializationWriter();
    writer.Write(myInt32Field);
    writer.Write(myDateTimeField);
    writer.Write(myStringField);
    // and so on

    info.AddValue("data", writer.ToArray());
}

反序列化基本上是相反的过程:在您的反序列化构造函数中,您检索 byte[] 并创建一个 SerializationReader 实例,将其传递给其构造函数。然后以写入的相同顺序检索数据。

protected EntityBase2(SerializationInfo info, StreamingContext context)
{
    SerializationReader reader =
      new SerializationReader((byte[]) info.GetValue("data",
                               typeof(byte[])));
    myInt32Field = reader.ReadInt32();
    myDateTimeField = reader.ReadDateTime();
    myString = reader.ReadString();
    // and so on
}

只需将 FastSerializer.cs 文件复制到您的项目中,然后更改命名空间以匹配即可使用。下载还包含一个 FastSerializerTest.cs 文件,其中包含 220 多个测试,但这不是必需的,只有在您想修改代码并确保不破坏任何内容时才应包含它。

(来自 v2.2)
每个类现在都有自己的文件。只需将 FastSerialization/Serialization 文件夹中的文件复制到您的项目中,然后更改命名空间以匹配即可使用。下载还包含 FastSerializer.UnitTest 文件夹下的 700 多个单元测试。

拥有的数据

我之前提到了“拥有的数据”的概念,所以让我们尝试定义一下:拥有的数据是对象的*数据*,它*是*

  • 任何值类型/结构体数据,例如 Int32ByteBoolean 等。由于值类型是结构体,在传递时会被重新创建,因此您的对象中的任何值类型数据都不会受到其他对象的影响,所以序列化/反序列化它们总是安全的。
  • 字符串。虽然它们是引用类型,但它们是不可变的(创建后无法更改),因此具有值类型语义,序列化它们总是安全的。
  • 由您的对象创建(或传递给您的对象)的、*从不*被外部对象使用的其他引用类型。这包括内部或私有的 HashtableArrayListArray 等,因为它们不能被外部对象访问。它还可以包括从外部创建并传递给您的对象以供其专有使用的对象。
  • 其他引用类型(同样由您的对象创建或传递给它)*可能*被其他对象使用,但您*知道*它们在反序列化过程中不会引起问题。这里的问题是,您的对象本身不知道在同一对象图中可能会序列化哪些其他对象。因此,如果您使用 SerializationWriter 将共享对象序列化到 byte[] 中,而该共享对象又被另一个外部对象使用其 SerializationWriter 序列化,那么最终将反序列化两个实例——每个实例一个不同的实例——因为序列化基础结构永远不会看到它们来检查和处理多个引用。

    这是否是问题取决于共享对象:如果共享对象是不可变的/没有字段数据,那么在反序列化过程中创建两个实例,尽管效率低下,但不会造成问题。但如果存在字段数据,并且这些字段数据应该由引用它的两个对象共享,那么这将是一个问题,因为每个对象现在都有自己的独立副本。最坏的情况是,当共享对象存储回指向引用它的对象的引用时,就会存在一个循环的风险,这会导致序列化很快失败并出现 OutOfMemoryExceptionStackOverflow。说了这么多,只有当多个引用对象被序列化在*同一*图中时,这才会成为一个问题。如果只有一个对象是图的一部分,那么被引用的对象可以被视为“拥有的数据”——其他引用变得无关紧要——但这取决于您来识别这种情况。

底线是确保只有经过仔细识别的“拥有的数据”存储在 SerializationWriter 中,让 .NET 序列化基础结构处理其余部分。

它是如何工作的(简短版)

SerializationWriter 有许多 Write(xxx) 方法,为多种类型重载。它还提供了一组 WriteOptimized(xxx) 方法,可以以更优化的方式存储某些类型,但可能对要存储的值有一些限制(这些限制已在方法中记录)。对于在编译时未知的数据,有一个 WriteObject(object) 方法,它将存储数据类型以及值,以便 SerializationReader 知道如何再次恢复数据。数据类型使用基于内部 enum SerializedType 的单个字节来存储。

SerializationReader 有许多方法与 SerializationWriter 上的方法相匹配。它们不能以相同的方式重载,所以每个都是一个单独的方法,其名称描述了它的用途。例如,使用 Write(string) 写入的 string 将使用 ReadString() 检索,使用 WriteOptimized(Int32) 写入的将使用 ReadOptimizedInt32() 检索,而 WriteObject(object) 将使用 ReadObject() 检索,依此类推。只要在 SerializationReader 上调用等效的数据检索方法,并且重要的是,顺序相同,您将获得与写入的数据完全相同的数据。

它是如何工作的(详细版)

Write(xxx) 方法使用类型的正常大小来存储数据,因此 Int32 始终占用 4 字节,Double 始终占用 8 字节,依此类推。WriteOptimized(xxx) 方法使用一种替代的存储方法,该方法不仅取决于类型,还取决于其值。因此,小于 128 的 Int32 可以存储在一个字节中(通过使用 7 位编码),但 268,435,456 或更大的 Int32 值或负数无法使用此技术存储(否则它们将占用 5 个字节,因此不能认为是可优化的!),但如果您想存储一个值,例如列表中的项目数,您*知道*它永远不会是负数且*永远*不会达到限制,那么请改用 WriteOptimized() 方法。

DateTime 是另一种具有优化方法的类型。它的限制是它无法优化精确到亚毫秒级的 DateTime 值,但对于仅存储日期而没有时间的常见情况,它在流中仅占用 3 个字节。(带有 hh:mm 但没有秒的 DateTime 将占用 5 个字节——仍然远优于 Write(DateTime) 方法占用的 8 个字节。)WriteObject() 方法使用 SerializedType 枚举来描述流中下一个对象的类型。该枚举定义为 byte,这为我们提供了 256 个可能的值。每种基本类型将占用其中一个值,但 256 个值已经很多了,所以我利用其中的一些来“编码”著名值及其类型。因此,每种数字类型都有一个“零”版本(有些也有“负一”版本),这使得单个字节就可以指定类型和值。这使得具有大量数据、类型在编译时未知的对象能够非常紧凑地序列化。

由于字符串被广泛使用,我特别关注它们以确保它们始终被优化(字符串没有 WriteOptimized(string) 重载,因为它们总是被优化!)。为此,我分配了 256 个值中的 128 个用于字符串使用——实际上是字符串列表。这允许使用字符串列表标记(由一个 byte 加上一个优化的 Int32 组成)写入任何字符串,以确保给定字符串值只写入一次——如果一个字符串出现多次,则只存储字符串列表标记多次。通过提供 128 个字符串列表,每个列表包含比一个包含许多字符串的字符串列表更少的字符串,字符串标记将仅为前 16,256 个唯一字符串占用 1 个字节,之后为接下来的 2,080,768 个唯一字符串占用 2 个字节!这应该够用了!已采取特别措施生成一个新列表,一旦当前列表达到 127 个字符串,就会生成一个新列表,以利用较小的字符串标记大小——一旦所有 128 个可能的列表都创建完毕,则以轮循方式将字符串分配给它们。字符串仅在长度大于两个字符时才进行标记——NullEmpty、“Y”、“N”或单个空格有自己的 SerializedTypeCodes,其他单字符字符串将占用 2 个字节(1 个用于类型,1 个用于字符本身)。

使用字符串标记的另一个大优点是,在反序列化期间,给定字符串的实例只会在内存中存储一次。虽然 .NET 序列化器对相同的字符串引用执行相同的操作,但它不会在引用不同但值相同的情况下执行此操作。当读取数据库表时,其中一列包含相同的字符串值,这很常见——因为它们通过 DataReader 在不同时间到达,它们具有相同的值但不同的引用,并且会被序列化多次。这对于 SerializationWriter 来说无关紧要——它使用 Hashtable 来识别相同的字符串,无论它们的引用如何。因此,反序列化的图通常比序列化之前的图更节省内存。

对象数组也受到了特别关注,因为它们在数据库类型工作中非常普遍,无论是作为 DataTable 还是实体类的一部分。它们的*内容*当然是使用 WriteObject() 方法写入的,以存储类型和/或优化值,但有一些特殊的优化,例如查找 nullDBNull.Value 的序列。在识别出序列时,将写入一个标识序列的 SerializationType(1 字节),然后是序列的长度(通常是 1 字节)。还有一个名为 WriteOptimized(object[], object[]) 的重载方法,它接受两个相同长度的对象数组,就像在修改的 DataRow 或修改的实体中发现的那样;第一个 object[] 按上述方式写入,但第二个列表中的值与第一个列表中的相应值进行比较,当值相同时,一个特定的 SerializationType 标识这一点,从而将每个相等对的大小减少到单个字节,无论其正常存储大小如何。

在开发 SerializationWriter 的过程中,我发现有必要序列化一个对象(一个工厂类),该对象将被集合中的许多实体使用。由于该工厂类没有自己的数据,只需要序列化其类型,但确保每个实体在反序列化期间都使用同一个工厂类将会很有帮助。为此,我添加了任何对象的标记:使用 WriteTokenizedObject(object) 将在流中放置一个标记来表示该对象,而对象本身将在字符串标记序列化之后才被序列化。我还为此方法添加了一个重载。

作为额外的空间节省器,如果您的对象可以通过无参数构造函数重新创建,那么请改用 WriteTokenizedObject(object, true) 重载;这将仅将类型名称存储为 stringSerializationReader 将直接使用 Activator.GetInstance 重新创建它。

只有一个可配置属性:SerializationWriter.OptimizeForSize(默认值为 true)控制 WriteObjectMethod() 是否应在可能的情况下使用优化序列化。由于它必须检查值是否在优化参数范围内,因此序列化需要花费更长的时间。将其设置为 false 将绕过这些检查,并使用快速简单的*方法*。实际上,对于少量数据,这些检查不会被注意到,对于大量数据(数*十兆*字节),这些检查只会花费几毫秒,所以通常情况下,请保持此属性不变。所有优化都已在代码中充分记录,特别是优化的要求,您应该将其视为工具提示。

优化黄金法则

以下是优化序列化时需要牢记的关键点列表

  1. 了解您的数据:通过了解您的数据、它的使用方式、可能的值范围、可能的值范围等,您可以识别“拥有的”数据,这将有助于决定使用哪些方法(或者,您甚至不应该使用任何方法,而是直接将数据序列化到 SerializationInfo 块中)。对于任何基本数据类型,总有一个非优化的*方法*可用,但使用优化的版本可以获得最佳结果。
  2. 以与写入相同的顺序读取数据:由于我们是*流式传输*数据而不是*将其与名称关联*,因此必须以与写入*完全相同*的顺序将其读回。实际上,您会发现这并不是什么大问题——如果*顺序*有问题,过程会*很快*失败,这将在*设计时*发现。
  3. 不要序列化任何不必要的内容:您无法比占用零*字节*的数据获得*更好的*优化!请参阅本文*稍后*的“优化技术”一节,了解示例。
  4. 考虑子对象是否可以视为“拥有的数据”:例如,如果您序列化一个集合,它的*内容*是集合的一部分,还是单独的对象,需要单独序列化并*仅*引用?这个考虑因素可能对序列化数据的大小*产生*重大影响,因为如果前者*为*真,那么一个*单一*的 SerializationWriter *实际*上*会*被*共享*用于*许多*对象,*并且*字符串标记的效果*可能*会*产生*显著*影响。请参阅本文*第二部分*作为示例。
  5. 请记住,序列化是一个黑盒过程:我的意思是,您序列化的数据不必与内存中的格式相同。只要在反序列化结束时,数据与序列化之前*相同*,*中间*发生了什么都没关系——*什么都可以*!通过序列化*足够*的数据来在另一端*重建*对象来优化。请参阅本文*稍后*的“优化技术”一节,了解示例。

SerializedType 枚举

下表显示了目前使用的 SerializedType 值。128 个值保留给字符串表,下面列出了 70 个,留下 58 个可供其他用途。

NullType 用于所有 null
NullSequenceType 内部用于标识对象数组中 null 值的序列
DBNullType 用于所有 DBNull.Value 实例
DBNullSequenceType 内部用于标识对象数组中 DBNull.Value 的序列(DataSet 大量使用此值)
OtherType 用于任何未识别的类型
BooleanTrueType BooleanFalseType 用于 Boolean 类型和值
ByteType SByteType CharType DecimalType DoubleType SingleType Int16Type Int32Type Int64Type UInt16Type UInt32Type UInt64Type 标准数字值类型
ZeroByteType ZeroSByteType ZeroCharType ZeroDecimalType ZeroDoubleType ZeroSingleType ZeroInt16Type ZeroInt32Type ZeroInt64Type ZeroUInt16Type ZeroUInt32Type ZeroUInt64Type 优化用于存储数字类型和零值
OneByteType OneSByteType OneDecimalType OneDoubleType OneSingleType OneInt16Type OneInt32Type OneInt64Type OneUInt16Type OneUInt32Type OneUInt64Type 优化用于存储数字类型和一值
MinusOneInt16Type MinusOneInt32Type MinusOneInt64Type 优化用于存储数字类型和负一值
OptimizedInt32Type OptimizedInt64Type OptimizedUInt32Type OptimizedUInt64Type 以最少的字节存储 32 位和 64 位类型?请参阅代码中的限制
EmptyStringType SingleSpaceType SingleCharStringType YStringType NStringType 优化用于高效存储单字符字符串(1 或 2 字节)
ObjectArrayType ByteArrayType CharArrayType 常见数组类型的优化
DateTimeType MinDateTimeType MaxDateTimeType DateTime 结构体,包含常用值
TimeSpanType ZeroTimeSpanType TimeSpan 结构体,包含常用值
GuidType EmptyGuidType GUID 结构体,包含常用值
BitVector32Type 优化用于将 BitVector32 存储在 1 到 4 字节中
DuplicateValueType 内部用于存储一对对象数组
BitArrayType 优化用于存储 BitArray
TypeType Type 存储为 string(非系统类型的*完整* AssemblyQualifiedName
SingleInstanceType 内部用于标识一个标记化对象应使用 Activator.GetInstance() 重新创建
ArrayListType ArrayList 的优化

优化技术

好的,我们已经识别了“拥有的数据”,并看到了如何使用标记和*著名*值来存储它们,所占用的*字节*比它们*实际*的*内存*大小*少*,但这*是否*还有其他*可以*做的*来*改进*优化?*当然*有......*让我们*看看*一个*示例*,*说明*黄金法则*#3* -*不要*序列化*任何*不*必要*的内容*

一个*直接*的*例子*是*一个*内部*用于*快速*定位*特定*项*(*基于*其*属性*)的* Hashtable。*那个* Hashtable *可以*很容易地*使用*反序列化*的数据*重新创建*,*所以*没有*必要*存储* Hashtable *本身*。*对于*大多数*其他*场景*,*问题*不是*序列化*本身*,*而是*反序列化*。*反序列化*需要*知道*数据*流*中*会*有什么*——*如果*它*不像*前面*示例*那样*是*隐式的*,*那么*您*需要*以*某种*方式*存储*该*信息*。

这就是 BitVector32 类:一个鲜为人知的类,它是您的朋友。有关*完整*信息,请参阅文档,但*基本*上*它*是*一个*结构体*,*占用*四个*字节*,*可以*通过*两种*方式*(*但*不能*同时*使用*!)*使用*——*它*可以*使用*其*32*位*来*存储*32*个*布尔*标志*,*或者*您可以*分配*多个*位*的*部分*来*打包*数据*(* SerializationWriter *中的* DateTime *优化*就*使用了*这种*技术*,*所以*请*查看*代码)。*在*其*布尔*标志*模式*下*,*它可以*非常*有*价值*地*识别*哪些*数据*位*实际*已*存储*,*并且*在*反序列化*时*,*您的*代码*可以*检查*标志*,*并*读取*预期*的数据*,*或者*采取*其他*操作*,*其中*其他*操作*可能*是*使用*默认*值*,*或*创建*空*对象*,*或*什么*都不*做*(*例如*,*默认*值*可能*已经*在*构造函数*中*创建*)。

使用 BitVector32 的*其他*好处是,布尔数据值存储为单个位,并且 BitVector32 可以*优化*存储(前提是*使用*的*位数*不超过* 21*位*——*否则*使用* Write(BitVector32) *来*获取*固定*的*四个*字节*),*因此*一个*使用*少于*8*个*标志*的* BitVector32 *将*仅*占用*一个*字节*!*同样*,*如果您*发现*需要*大量*标志*,*比如*您*有*一个*大的*对象*列表*并且*需要*为*每个*对象*存储*一个*布尔*值*,*那么*使用* BitArray *,*它*仍然*每个*项*只*使用*一个*位*(*只*是*四舍五入*到*最近*的*字节*),*但*可以*存储*许多*许多*位*。

作为*一个*例子*说明*位*标志*有多*有用*,*这里*是*我*将在*第二部分*中*介绍*的*快速* DataSet *序列化器*的一些*示例*代码*:*标志*是*使用* BitVector32.CreateMask() *方法*创建*的*,*该*方法*经过*重载*以*将*后续*蒙版*链接*到*前面的*蒙版*。*它们*是* static *和*只读*的*,*因此*内存*效率*高*。*这一*套*标志*是*针对* DataColumn *的*:*每*个*序列化*的*列*将*占用*两个*字节*,*但*请*注意*,*某些*数据*(*如* AllowNull *和* ReadOnly *)*已经*由*标志*本身*序列化*,*而*其他*数据*现在*将*仅*有条件*地*序列化*。*实际上*,*一个*位*标志*(* HasAutoIncrement *)*用于*有条件*地*序列化*三*个*数据*(* AutoIncrement *、* AutoIncrementSeed *和* AutoIncrementStep *)。

static readonly int MappingTypeIsNotElement

= BitVector32.CreateMask();
static readonly int AllowNull = BitVector32.CreateMask(MappingTypeIsNotElement);
static readonly int HasAutoIncrement = BitVector32.CreateMask(AllowNull);
static readonly int HasCaption = BitVector32.CreateMask(HasAutoIncrement);
static readonly int HasColumnUri = BitVector32.CreateMask(HasCaption);
static readonly int ColumnHasPrefix = BitVector32.CreateMask(HasColumnUri);
static readonly int HasDefaultValue = BitVector32.CreateMask(ColumnHasPrefix);
static readonly int ColumnIsReadOnly =
                BitVector32.CreateMask(HasDefaultValue);
static readonly int HasMaxLength = BitVector32.CreateMask(ColumnIsReadOnly);
static readonly int DataTypeIsNotString = BitVector32.CreateMask(HasMaxLength);
static readonly int ColumnHasExpression =
                BitVector32.CreateMask(DataTypeIsNotString);
static readonly int ColumnHasExtendedProperties =
                BitVector32.CreateMask(ColumnHasExpression);

static BitVector32 GetColumnFlags(DataColumn dataColumn)
{
  BitVector32 flags = new BitVector32();
  flags[MappingTypeIsNotElement] =
        dataColumn.ColumnMapping != MappingType.Element;
  flags[AllowNull] = dataColumn.AllowDBNull;
  flags[HasAutoIncrement] = dataColumn.AutoIncrement;
  flags[HasCaption] = dataColumn.Caption != dataColumn.ColumnName;
  flags[HasColumnUri] = ColumnUriFieldInfo.GetValue(dataColumn) != null;
  flags[ColumnHasPrefix] = dataColumn.Prefix != string.Empty;
  flags[HasDefaultValue] = dataColumn.DefaultValue != DBNull.Value;
  flags[ColumnIsReadOnly] = dataColumn.ReadOnly;
  flags[HasMaxLength] = dataColumn.MaxLength != -1;
  flags[DataTypeIsNotString] = dataColumn.DataType != typeof(string);
  flags[ColumnHasExpression] = dataColumn.Expression != string.Empty;
  flags[ColumnHasExtendedProperties] =
        dataColumn.ExtendedProperties.Count != 0;
  return flags;
}

以下*是*使用*这些*标志*序列化/反序列化* DataTable *所有*列*的*方法*:*您*可以看到*这些*标志*被*用于*将*可选*数据*的*序列化*与*必需*数据*(*如* ColumnName *)*和*默认*数据*(*如* DataType *)*结合*起来*,*其中*数据*始终*是*必需*的*,*但*仅*在*它*不是*我们*选择*的*默认值*(*此处*为* typeof(string) *)*时*才*需要*序列化*。

void SerializeColumns(DataTable table)
{
  DataColumnCollection columns = table.Columns;
  writer.WriteOptimized(columns.Count);

  foreach(DataColumn column in columns)
  {
    BitVector32 flags = GetColumnFlags(column);
    writer.WriteOptimized(flags);

    writer.WriteString(column.ColumnName);
    if (flags[DataTypeIsNotString])
        writer.Write(column.DataType.FullName);
    if (flags[ColumnHasExpression])
        writer.Write(column.Expression);
    if (flags[MappingTypeIsNotElement])
        writer.WriteOptimized((int) MappingType.Element);

    if (flags[HasAutoIncrement]) {
      writer.Write(column.AutoIncrementSeed);
      writer.Write(column.AutoIncrementStep);
    }

    if (flags[HasCaption]) writer.Write(column.Caption);
    if (flags[HasColumnUri])
        writer.Write((string) ColumnUriFieldInfo.GetValue(column));
    if (flags[ColumnHasPrefix]) writer.Write(column.Prefix);
    if (flags[HasDefaultValue]) writer.WriteObject(column.DefaultValue);
    if (flags[HasMaxLength]) writer.WriteOptimized(column.MaxLength);
    if (flags[TableHasExtendedProperties])
        SerializeExtendedProperties(column.ExtendedProperties);
  }
}
void DeserializeColumns(DataTable table)
{
  int count = reader.ReadOptimizedInt32();
  DataColumn[] dataColumns = new DataColumn[count];
  for(int i = 0; i < count; i++)
  {
    DataColumn column = null;
    string columnName;
    Type dataType;
    string expression;
    MappingType mappingType;

    BitVector32 flags = reader.ReadOptimizedBitVector32();
    columnName = reader.ReadString();
    dataType = flags[DataTypeIsNotString] ?
               Type.GetType(reader.ReadString()) :
               typeof(string);
    expression = flags[ColumnHasExpression] ?
                 reader.ReadString() : string.Empty;
    mappingType = flags[MappingTypeIsNotElement] ?
                  (MappingType) reader.ReadOptimizedInt32() :
                  MappingType.Element;

    column = new DataColumn(columnName, dataType,
                            expression, mappingType);
    column.AllowDBNull = flags[AllowNull];
    if (flags[HasAutoIncrement]) {
        column.AutoIncrement = true;
        column.AutoIncrementSeed = reader.ReadInt64();
        column.AutoIncrementStep = reader.ReadInt64();
    }
    if (flags[HasCaption])
        column.Caption = reader.ReadString();
    if (flags[HasColumnUri])
        ColumnUriFieldInfo.SetValue(column, reader.ReadString());
    if (flags[ColumnHasPrefix])
        column.Prefix = reader.ReadString();
    if (flags[HasDefaultValue])
        column.DefaultValue = reader.ReadObject();
    column.ReadOnly = flags[ColumnIsReadOnly];
    if (flags[HasMaxLength])
        column.MaxLength = reader.ReadOptimizedInt32();
    if (flags[TableHasExtendedProperties])
        DeserializeExtendedProperties(column.ExtendedProperties);

    dataColumns[i] = column;
  }
  table.Columns.AddRange(dataColumns);
}

在*本文*的*第二部分*中*,*我*将*更*详细*地*介绍*使用*位*标志*和*序列化*子对象*,*以*充分*利用*本文*中*列出*的*优化*功能*。

请*随时*在此*处*的代码*项目*上*发布*评论/改进*建议*。

v1 到 v2 的更改

  • 添加了对 .NET 2.0 的支持,使用了条件编译
    在项目属性(“生成”选项卡下的“常规”)中向条件编译符号添加“NET20”,或搜索“#if NET20”并手动删除不需要的代码和条件构造。
  • 支持 .NET 2.0 DateTime,包括 DateTimeKind
  • 添加了对*类型化*数组的支持。
  • 添加了对*可空*泛型类型的支持。
  • 添加了对 List<T>Dictionary<K,V> 泛型类型的支持。
  • 添加了可选数据*压缩*的支持 - 有关详细信息,请参阅*下面的* MiniLZO *部分*。
  • 添加了 IOwnedDataSerializableAndRecreatable 接口,以允许类和结构被识别为可以完全*自行*序列化/反序列化的类型
  • 为所有*新*功能添加了*测试*
  • 修复了一个*已知*的*bug*(BitArray 被*反序列化*但*四舍五入*到*最近*的*8*位*)。

有关*所有*更改*的*详细信息*,*请*参阅*下面的*“*历史*”*部分*。

v2 到 v2.1 的更改

  • WriteOptimized(decimal) 中的*bug*修复,*在*某些*情况*下*使用了*不正确*的*标志*。
    感谢 marcin.rawicki 和 DdlSmurf 发现了这个问题。
  • WriteOptimized(decimal value) 现在*可能*的情况下*将*以*零**小数*位*存储*十进制*值*。
    此*优化*基于*这样一个*事实*:*给定*的*十进制*数*可以*以*不同*的*方式*存储*,*具体*取决于*它*如何*创建*。*例如*,“2”*可以*存储*为*“2”*且*无*缩放*(*如果*创建*为* decimal d = 2g *)*或*“200”*且*缩放*为*“2”*(*如果*使用* decimal d = 2.00g *创建*)。
    从* SQL Server *检索*的数据*会*保留*缩放*,*因此*通常*会*在*内存*中使用*后者*格式*存储*。
    这*两种*方式*之间*绝对*没有*数值*差异*,*唯一*可见*的*影响*是*当*您*不*使用*特定*格式*化*来*显示*数字*时*(*使用* ToString() *)。*然而*,*从*优化*序列化*的*角度*来看*,“2”*比*“200”*可以*更*有效地*存储*,*因此*代码*将*执行*一个*简单*的*检查*以*确定*此*条件*并*在*可能*时*使用*零*缩放*。
    此*优化*默认*启用*,*但我*添加*了*一个* static *布尔*属性* DefaultPreserveDecimalScale *,*以*允许*在*需要*时*禁用*它*。
  • 负整数*现在*可以*优化*存储*。
    此*优化*使用*二进制*补码*来*转换*负*数*,*并且*(*前提*是*现在*的*正*数*可以*优化*)*将*使用*不同*的* TypeCode *来*存储*类型*和*应该*在*反序列化*时*取反*的事实*。
  • Int16/UInt16*现在*可以*优化*存储*。
    当然,这里的*潜在**减少*非常*有限*,*但*它们*包括*以*完整*性*计*。*这也*包括*类型化*数组*支持*和*负*数*。
  • 使用* WriteObject(object) *存储*的* Enum *值*现在*会自动*优化*(*如果*可能*)。
    会*检查*以*确定*整数*值*是否*可以*优化*,*然后*存储* Enum *类型*以及*优化*或*未优化*的值*。*由于* Enum *通常*是非负*的*且*范围*有限*,*优化*将*大部分*时间*可用*。*存储* Enum *类型*也将*允许*反序列化器*确定*底层*整数*类型*并*获取*正确*的*大小*值*。
  • 添加了对“类型*代理*”的支持,*允许*编写*外部*辅助*类*,*这些*类*知道*如何*序列化/反序列化*特定* Type *或*一组* Types*。
    这是一个*相对*简单*的*功能*,*但*具有*巨大*的*潜力*,*可以在*不*消耗*有限*的*类型*代码*、*不*修改*快速*序列化*代码*且*无需*控制*被*序列化*的* Type *的*情况*下*扩展*对*非*直接*支持*类型*的*支持*。
    这个*功能*我*一直*想*实现*,*但*特别*感谢*Dan Avni*给*了我*现在*去做*的*好*理由*,*并*在*实际*应用*中*提供*了*反馈*和*测试*。

    检查了*实现*目标*的*多种*方法*,*包括*委托*字典*;*以及*一组*替代*类型*代码*,*但*所选*的*实现*允许*良好*的*模块化*和*代码*重用*。

    创建了一个*新*接口*,*名为* IFastSerializationTypeSurrogate *,*它*只有*三个*成员*
    • bool SupportsType(Type) *,*允许* SerializationWriter *查询*辅助*类*以*查看*给定* Type *是否*受*支持*
    • void Serialize(SerializationWriter writer, object value) *,*它*执行*序列化*;*和*
    • object Deserialize(SerializationReader reader, Type type) *,*它*执行*反序列化*

    可以使用*任意*数量*的*类型*代理*辅助*类*,*它们*只需*添加到* SerializationWriter *上的*静态*属性* TypeSurrogates *(*对于* NET 1.1 *无需*在* SerializationReader *上*重复*)*,*它*是* List<IFastSerializationTypeSurrogate> *或* ArrayList*。

    该*想法*是*,*类型*代理*辅助*类*在*应用程序*开始*时*添加*一次*,*并且*当* WriteObject *用尽*其*已知*类型*列表*并且*会*使用*普通* BinaryFormatter *时*,*它*将*首先*查询*列表*中的*每个*辅助*程序*以*查看* Type *是否*受*支持*。
    如果*找到*匹配*项*,*则*将*存储* TypeCode *,*然后*存储*对象的* Type *,*然后*在*辅助*程序*上*调用* Serialize *方法*来*执行*实际*工作*。*反序列化*是*相反*的过程*,*并且*必须*提供*同一*组*辅助*程序*来*执行*反序列化*。

    下载*中*有*几个*示例*类型*代理*辅助*类*,*涵盖* Color *、* Pair *、* Triplet *、* StateBag *、* Unit *和* Hashtable *的*简单*实现*。*实现* IFastSerializationTypeSurrogate *接口*的*类*的*结构*可以*有*多种*方式*,*但*通过*使*序列化/反序列化*的*实现*代码*也*可通过* public static *方法*访问*,*辅助*类*还*可以*用于*序列化*在*设计时*已知*的* Type *,*可能*作为*支持* IOwnedDataSerializable *的*更大*类*的*一部分*。

    如果您*创建*一个*有用*的*类型*代理*辅助*类*,*您*可能*想*在此*处*发布*它*,*以便*可以*与*其他人*共享*,*从而*避免*重复*造轮子*。

v2.1 到 v2.2 的更改

  • 现在*可以*将*任何* Stream *传递*给* SerializationReader *和* SerializationWriter*。
    • 已*删除*对*流*起始*位置*的*假设*。*起始*位置*会*被*存储*并*相对*使用*。
    • SerializationWriter *和* SerializationReader *都不*需要*可查找*的*流*。*传递*不可查找*的*流*只会*意味着**无法*更新**头*部*。
    • ToArray() *仍*将*仅*返回* SerializationWriter *写入*的部分*流*。
  • 存储*的*数据*流*已*变得*更*线性*。*现在*有一个*4*字节*或*12*字节*的*头部*,*紧接着*是*序列化*数据*。
    • 标记化*的*字符串*和*对象*现在*在*首次*遇到*时*内联*写入*,*而不是*一次性*附加*到*末尾*。
    • 标记化*字符串*和*标记化*对象*的*计数*(*用于* SerializationReader *中的*表*列表*的*预*大小*调整*)*现在*存储*在*正常*情况*下*使用* MemoryStream *的*头部*中*。
      (*对于*替代*流*(*例如*压缩*流*)*或*在*相关*构造函数*中*将* allowUpdateHeader *设置*为* false *的情况*下*,*头部*将*不会*被*更新*。*在这种*情况*下*,*您*可以*在* SerializationReader *构造函数*中*指定*预*大小*信息*,*方法*是*从* SerializationWriter *将*最终*表*大小*从*流*外部*传递*,*或*进行*合理*的*猜测*。*或者*,*您可以*根本*不*指定*预*大小*信息*,*让*表*在*标记化*项*从*数据*流*中*拉出*时*增长*,*尽管*这*可能*会*浪费*内存*且*不*推荐*。)
    • SerializationReader *现在*是*一个*前向*流*读取器*——*无需*跳转*到*流*末尾*再*回来*。
    • 一旦*数据*由* SerializationReader *从* Stream *反序列化*后*,*流*的位置*将*处于*正确*的位置*,*以*便*进行*任何*后续*数据*——*无需*跳过*令牌*表*。
    • 对于*正常的* MemoryStream *情况*,*将*使用*一个*12*字节*的*头部*:*一个* Int32 *用于*序列化*数据*的总*长度*;*一个* Int32 *用于* String *表*大小*;*以及*一个* Int32 *用于*对象*表*大小*。
      如果*不*更新*头部*,*则*将*只有*一个*4*字节*的* Int32 *头部*,*其*值为*零*。
  • 用* UpdateHeader() *替换*了* AppendTokenTables() *,*因为*不再*进行*附加*操作*。

MiniLZO - 实时压缩

v2*源代码*中*包含*一个*名为* MiniLZO.cs *的文件*,*其中*包含* Astaelan *在* 纯 C# MiniLZO 移植 *文章*中的*代码*的*稍作*修改*版本*。
该*文章*的代码*是*原始*C*版本的*直接*移植*,*因此*不*存储*原始*未压缩*大小*。

我*所*做的*修改*如下*

  • 修改*了*方法*签名*,*以*允许*压缩*字节*数组*的*任何*部分*。
  • 将*未压缩*大小*存储*在*压缩*数据*末尾*的*4*字节*中*。
  • 添加*了一个*特殊*的方法*重载*,*它*接受*一个* MemoryStream *并*使用*其*内部* byte[] *缓冲区*作为*源*数据*。
    此外*,*它*会*查看*此*缓冲区*中*未*使用*的*字节*,*并*在*可能*时*使用*这些*字节*进行*原地*压缩*,*从而*节省*内存*。

重要的是要注意*

  • MiniLZO*受* GNU*通用*公共*许可证*保护*。*本文*中*的*其他*代码*则*不受*此*限制*——*您可以*随意*使用*它*。
  • MiniLZO*使用*“*不安全*”*代码*。“*不安全*”*仅*在* .NET *意义*上*,*意味着*代码*使用*指针*,*因此*无法*由* .NET *运行时*保证*不会*损坏*内存*。
    在这种*情况*下*,*它*相当*安全*,*因为*它*会*检测*指针*是否*超出*字节*数组*范围*并*抛出*异常*。
    它*所*包含*的项目*(*无论是*单独*的*DLL*项目*还是*现有*项目*)*都需要*选中*不安全*选项*才能*编译*。

它的*优点*是*,*作为*一个*仅*基于*内存*缓冲区*的*压缩器*,*它*执行*压缩*速度*极快*,*解压*速度*更快*。
在我*的*测试*中*,*我*获得了*大约*45%-55%*的*大小*缩减*,*这*比*其他*流式*压缩器*在*其*最快*设置*下*要*快*得多*。
其他*压缩器*可能*产生*稍好*的*压缩*效果*,*但*以*速度*降低*为*代价*。
压缩*的使用*完全*是*可选*的*——*但是*,*如果您*知道*您*将*以*某种*方式*压缩*序列化*数据*,*请*确保*将* SerializationWriter.OptimizeForSize *静态*属性*设置*为* false *以*获得*最佳*结果*。

要*使用*此*压缩*,*以前*使用*此*代码*的地方*...

  byte[] serializedData = mySerializationWriter.ToArray();

...*改为*这样做*...

// To ensure that all required data is stored

mySerializationWriter.AppendTokenTables();
byte[] serializedData =
       MiniLZO.Compress((MemoryStream) writer.BaseStream);

反序列化*甚至*更*简单*...

  byte[] serializedData = MiniLZO.Decompress(serializedData);

历史

  • 2006-09-25 v1.0 在 CodeProject 上发布
  • 2006-11-27 v2.0 在 CodeProject 上发布
    • 添加了* MiniLZO.cs *用于*实时*压缩*
    • 修复*:*修复了* BitArray *序列化*中的*bug* -*将*四舍五入*到*最近*的*8*位*
    • 添加了*必需*的* .NET 2.0 *条件*代码*
    • 添加了*静态* DefaultOptimizedForSize *布尔*属性* -*由* OptimizeForSize *属性*使用*
    • 将* DateHasTimeMask *重命名*为* DateHasTimeOrKindMask
    • 添加了*内部* UniqueStringList *类*以*加快*字符串*标记*匹配*
      • 字符串*标记*现在*假定*固定*的*128*个*字符串*列表*并*使用*轮循*分配*
      • 使用*算术*进行*反序列化*而不是*多维*数组*——*证明*更快*
      • 调整*了*哈希表*大小*——*低*尺寸*时*翻*四*倍*,*然后*恢复*到*翻*倍*。
    • 添加*了* WriteOptimized(string) *重载*
      • Write(string) *调用*新*方法*而不是*反之*,*因为*新*方法*不是*虚拟*的*,*因此*稍快*一些*。*命名*也*一致*
    • 在*合适*的地方*添加*了* CLSCompliant *属性*
    • 重新*组织*了*所有*方法*(*没有*改进*但*更容易*关联*方法*类型*)
    • 添加*了*新的*序列化*类型*
      • DuplicateValueSequenceType
      • ObjectArrayType
      • EmptyTypedArrayType
      • EmptyObjectArrayType
      • NonOptimizedTypedArrayType
      • FullyOptimizedtypedArrayType
      • PartiallyOptimizedTypedArrayType
      • OtherTypedArrayType
      • BooleanArrayType
      • ByteArrayType
      • CharArrayType
      • DateTimeArrayType
      • DecimalArrayType
      • DoubleArrayType
      • SingleArrayType
      • GuidArrayType
      • Int16ArrayType
      • Int32ArrayType
      • Int64ArrayType
      • SByteArrayType
      • TimeSpanArrayType
      • UInt16ArrayType
      • UInt32ArrayType
      • UInt64ArrayType
      • OwnedDataSerializableAndRecreatableType
    • 添加*了*占位符*序列化*类型*以*显示*剩余*的*数量*(*目前*31*个*)
    • 添加*了* IOwnedDataSerializableAndRecreatable *接口*
    • 重构*了* processObject *代码*以*将*数组*确定*保留*在*单独*的方法*中*,*以便*可以*重用*
    • 完全*支持* .NET 2.0 *日期*(*包括* DateTimeKind *)*
    • WriteOptimized(object[], object[]) *已*更新*
      • 已*稍微*优化*
      • 现在*查找*重复*值*的*序列*
    • writeObjectArray(object[]) *已*更新*
      • 已*稍微*优化*
      • 现在*查找*重复*值*的*序列*
    • 现在*到处*使用* .Equals *而不是* ==
    • 添加*了*对*所有*基本/内置*类型*的*数组*的支持*
    • 添加*了*对*结构体*和*结构体*数组*的支持*
    • 添加*了*对*实现* IOwnedDataSerializableAndRecreatable *的*类*的*数组*的支持*
    • 添加*了*对* Dictionary<K,V> *的支持*
      • Write<K,V>(Dictionary<K,V> value) *方法*
      • Dictionary<K,V>ReadDictionary<K,V>() *方法*用于*简单*创建* Dictionary<K,V>
      • ReadDictionary<K,V>(Dictionary<K,V> dictionary) *方法*用于*填充*预先*创建*的* Dictionary
    • 添加*了*对* List<T> *的支持*。
      • Write<T>(List<T> value) *方法*
      • List<T> ReadList<T>() *方法*
    • 添加*了*对* Nullable<T> *的支持*
      • WriteNullable(ValueType value) -*仅*调用* WriteObject(value) *但*包含*以*示*清晰*
      • 为*所有*基本/内置*类型*提供*完整的* ReadNullableXXX() *方法*列表*
    • 重构*了* .NET 2.0 *协变*“*问题*”*的*数组*处理*方式*
    • ToArray() *重构*为*将*令牌*写入*与*返回*字节*数组*分开*。*允许*任何*外部*压缩*例程*在*需要*时*处理* MemoryStream
    • 重新*排序*了* WriteObject() *中的*处理*——*特别是* DbNull *具有*更高*的*优先级*
    • 测试*套件*已*更新*,*添加*了*大量*测试*
  • 2007-02-25 v2.1 在 CodeProject 上发布
    • 修复*:*修复了* WriteOptimized(decimal) *中的*bug*
    • 添加/更新*了*一些*注释*。
    • 添加*了*优化*,*以*在*可能*时*存储*不*带*小数*的* Decimal*
      • 添加*了* static DefaultPreserveDecimalScale *属性*——*默认*为* false
      • 添加*了* public PreserveDecimalScale *属性*,*它*将*其*初始*值*设置*为*静态*默认值*,*但*允许*按*实例*进行*配置*
      • 更新*了* WriteObject() *以*始终*优化*存储* Decimal *,*因为*总会有*节省*
      • 由于*同一*原因*,*移除了* OptimizedDecimalType *类型*代码*
    • 添加*了*对*优化* Int16/UInt16 *值*的支持*
      • 添加*了* HighestOptimizable16BitValue *的*公共*常量*
      • 添加*了* OptimizationFailure16BitValue *的*内部*常量*
      • 更新*了* WriteObject *中的*代码*以*查找*可*优化*的*16*位*值*
      • 添加*了* WriteOptimized(Int16) *和* WriteOptimized(UInt16) *方法*
      • 添加*了* WriteOptimized(Int16[]) *和* WriteOptimized(UInt16[]) *方法*
      • 添加*了* ReadOptimizedInt16() *和* ReadOptimizedUInt16() *方法*
      • 添加*了* ReadOptimizedInt16Array() *和* ReadOptimizedUInt16Array() *方法*
      • 更新*了* ReadInt16Array() *和* ReadUInt16Array() *方法*
      • 添加*了*新的*序列化*类型*
        • OptimizedInt16Type
        • OptimizedUInt16Type
    • 添加*了*对*某些*负*整数*值*的支持*。
      • 更新*了* WriteObject *中的*代码*以*查找*可*优化*的*负*值*
      • 添加*了*新的*序列化*类型*
        • OptimizedInt16NegativeType
        • OptimizedInt32NegativeType
        • OptimizedInt64NegativeType
    • 添加*了*对* Enum *类型的*支持*
      • 更新*了* WriteObject *以*查找* Enum *值*并*将*它们*存储*为其* Type *和*整数*值*——*尽可能*优化*
      • 添加*了*新的*序列化*类型*
        • EnumType
        • OptimizedEnumType
    • 添加*了*对*类型*代理*助手*的支持*
      • 添加*了* IFastSerializationTypeSurrogate *接口*
      • 添加*了* TypeSurrogates *静态*属性*。(*NET 2.0 *为* List<IFastSerializationTypeSurrogate> *,*NET 1.1 *为* ArrayList *)*
      • 更新*了* WriteObject *以*查询* TypeSurrogates *中的*助手*,*然后*才*使用* BinarySerializer *作为*最后的*手段*。
      • 添加*了*内部* static *方法*以*查找*给定*类型*的*辅助*类*——*由* SerializationWriter *和* SerializationReader *共享*。
      • 添加*了*新的*序列化*类型*
        • SurrogateHandledType
  • 2010-05 v2.2 在 CodeProject 上发布
    • 修正了*一些*拼写*错误*,*并*更改*了*一些*文章*文本*(*针对* v2.2 *的版本*)
    • 移除了*条件*编译*——*现在*仅*支持* .NET 2.0 *或*更高*版本*
    • 将*类*分离*到*不同*的文件*中*
    • 重命名*了*一些*方法*(*我*真的*用*了*小写*方法*名*吗*!?*)
    • 尽可能*使用* Switch *而不是*嵌套*的* if/else*。
    • 使用了* var*。
    • 发布*现在*包含*两个*项目*:*一个*用于*代码*,*一个*用于*单元*测试*。*使用* VS2010 *,*但*代码*位于*子*文件夹*中*,*易于*提取*。
    • 用* UpdateHeader() *替换*了* AppendTokenTables()*
    • 将*令牌*表*内联*存储*解决了*Simon Thorogood*报告*的*问题*,*即*由* IOwnedDataSerializableAndRecreatable *类*写入*的*令牌化*字符串*未*写入* stream*
    • 添加*了*对*任何*流*和*任何*起始*位置*的支持*。

© . All rights reserved.