结构体的快速序列化/反序列化

12

我有大量地理数据,它们以简单的对象结构表示,仅由结构体组成。 我所有的字段都是值类型。

public struct Child
{
   readonly float X;
   readonly float Y;
   readonly int myField;
}

public struct Parent
{
   readonly int id;
   readonly int field1;
   readonly int field2;
   readonly Child[] children;
}
数据已经被切块以适合小部件Parent[]。每个数组包含一些千的父亲实例。我有太多的数据无法全部保存在内存中,所以我需要来回交换这些块到磁盘上。(一个文件大约会产生2-300KB)。
对于将Parent[]序列化/反序列化为byte[]并转储到磁盘上并且快速反序列化速度尤为重要的情况下,什么是最有效的方法?
简单的BinarySerializer是否足够好?还是应该通过StructLayout(请参阅已接受的答案)进行hack操作?我不确定是否可以对Parent.children的数组字段使用它?
更新: 响应评论 - 是的,对象是不可变的(代码已更新),而且确实children字段不是值类型。 300KB听起来不算多,但我有很多像那样的文件,所以速度很重要。

4
所有我的字段都是值类型 - children 字段不是值类型。 - H H
1
300KB是一个很小的数量,在没有优化的情况下,这个数量可以在0.1秒内进行反序列化/序列化。 - Serj-Tm
二进制序列化器非常慢。它使用反射将元数据注入序列化数据中。由于它不提供类型元数据,因此XML序列化器以比二进制序列化器更小、更快的格式序列化数据。在任何情况下,序列化都是通过反射完成的,速度非常慢。我的一位聪明的同事创建了一种自定义的二进制序列化机制,比XML序列化器快20倍,比二进制序列化器更快。它也显著更小。 - Ritch Melton
2个回答

14

如果您不想采取自己编写序列化程序的方式,您可以使用protobuf.net序列化器。下面是一个小测试程序的输出结果:

Using 3000 parents, each with 5 children
BinaryFormatter Serialized in: 00:00:00.1250000
Memory stream 486218 B
BinaryFormatter Deserialized in: 00:00:00.1718750

ProfoBuf Serialized in: 00:00:00.1406250
Memory stream 318247 B
ProfoBuf Deserialized in: 00:00:00.0312500

这应该是相当易懂的。这只是一个运行,但是相当有指示性,我看到了加速(3-5倍)。

要使您的结构可序列化(使用protobuf.net),只需添加以下属性:

[ProtoContract]
[Serializable]
public struct Child
{
    [ProtoMember(1)] public float X;
    [ProtoMember(2)] public float Y;
    [ProtoMember(3)] public int myField;
}

[ProtoContract]
[Serializable]
public struct Parent
{
    [ProtoMember(1)] public int id;
    [ProtoMember(2)] public int field1;
    [ProtoMember(3)] public int field2;
    [ProtoMember(4)] public Child[] children;
}

更新:

事实上,编写自定义序列化程序非常容易,以下是一个基本实现:

class CustSerializer
{
    public void Serialize(Stream stream, Parent[] parents, int childCount)
    {
        BinaryWriter sw = new BinaryWriter(stream);
        foreach (var parent in parents)
        {
            sw.Write(parent.id);
            sw.Write(parent.field1);
            sw.Write(parent.field2);

            foreach (var child in parent.children)
            {
                sw.Write(child.myField);
                sw.Write(child.X);
                sw.Write(child.Y);
            }
        }
    }

    public Parent[] Deserialize(Stream stream, int parentCount, int childCount)
    {
        BinaryReader br = new BinaryReader(stream);
        Parent[] parents = new Parent[parentCount];

        for (int i = 0; i < parentCount; i++)
        {
            var parent = new Parent();
            parent.id = br.ReadInt32();
            parent.field1 = br.ReadInt32();
            parent.field2 = br.ReadInt32();
            parent.children = new Child[childCount];

            for (int j = 0; j < childCount; j++)
            {
                var child = new Child();
                child.myField = br.ReadInt32();
                child.X = br.ReadSingle();
                child.Y = br.ReadSingle();
                parent.children[j] = child;
            }

            parents[i] = parent;
        }
        return parents;
    }
}

当在一个简单的速度测试中运行时,它的输出如下:

Custom Serialized in: 00:00:00 
Memory stream 216000 B 
Custom Deserialized in: 00:00:00.0156250

显然,这种方法比其他方法的灵活性要少得多,但如果速度真的很重要,它比protobuf方法快2-3倍。它还可以产生最小的文件大小,因此写入磁盘应该更快。


1
Protobuf在大多数情况下是易于使用和性能之间的一个很好的折衷方案。如果你想要疯狂地优化,它仍然无法击败定制解决方案的性能。特别是那些可以完全零成本的位可复制的解决方案! - usr
你难道没有检查过它是64位机器还是32位的吗? - Cool guy

11

BinarySerializer是一个非常通用的序列化程序。它的性能不如自定义实现。

幸运的是,您的数据仅包含结构体。这意味着您将能够为Child修复一个structlayout,并使用unsafe代码从磁盘上读取的byte[]中进行位拷贝孩子数组。

对于父级,情况并不那么简单,因为您需要分别处理每个子项。我建议您使用unsafe代码从您读取的byte[]中复制可位拷贝字段,并单独反序列化子项。

您是否考虑过使用内存映射文件将所有子项映射到内存中?然后,您可以重用操作系统的缓存功能,而无需处理读取和写入。

零拷贝反序列化一个Child[]的步骤如下:

byte[] bytes = GetFromDisk();
fixed (byte* bytePtr = bytes) {
 Child* childPtr = (Child*)bytePtr;
 //now treat the childPtr as an array:
 var x123 = childPtr[123].X;

 //if we need a real array that can be passed around, we need to copy:
 var childArray = new Child[GetLengthOfDeserializedData()];
 for (i = [0..length]) {
  childArray[i] = childPtr[i];
 }
}

我查了一下内存映射文件,它们在磁盘访问管理方面看起来非常棒!你能写一个不安全段的例子吗?在不安全模式下,如何将byte[]“转换”为Child[]?因为正如你所说,这将需要零时间。 - user256890
1
我添加了一个例子。如果你想要零拷贝,你需要改变你的应用程序来使用指针或者不安全的IO,使用ReadFile(直接读入现有的Child[])。但是我的猜测是单次复制实际上并不会有太大影响。CPU在这方面表现得很好。 - usr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接