【问题标题】:Improve Binary Serialization Performance for large List of structs提高大型结构列表的二进制序列化性能
【发布时间】:2011-09-22 15:26:01
【问题描述】:

我有一个将 3d 坐标保存在 3 个整数中的结构。在测试中,我将 100 万个随机点的 List 放在一起,然后将二进制序列化用于内存流。

内存流大约为 21 MB - 这似乎非常低效,因为 1000000 点 * 3 个坐标 * 4 个字节应该以至少 11MB 出现

在我的测试台上也需要大约 3 秒。

有任何提高性能和/或尺寸的想法吗?

(如果有帮助,我不必保留 ISerialzable 接口,我可以直接写出到内存流中)

编辑 - 根据下面的答案,我整理了一个序列化摊牌,比较 BinaryFormatter、'Raw' BinaryWriter 和 Protobuf

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using ProtoBuf;

namespace asp_heatmap.test
{
    [Serializable()] // For .NET BinaryFormatter
    [ProtoContract] // For Protobuf
    public class Coordinates : ISerializable
    {
        [Serializable()]
        [ProtoContract]
        public struct CoOrd
        {
            public CoOrd(int x, int y, int z)
            {
                this.x = x;
                this.y = y;
                this.z = z;
            }
            [ProtoMember(1)]            
            public int x;
            [ProtoMember(2)]
            public int y;
            [ProtoMember(3)]
            public int z;
        }

        internal Coordinates()
        {
        }

        [ProtoMember(1)]
        public List<CoOrd> Coords = new List<CoOrd>();

        public void SetupTestArray()
        {
            Random r = new Random();
            List<CoOrd> coordinates = new List<CoOrd>();
            for (int i = 0; i < 1000000; i++)
            {
                Coords.Add(new CoOrd(r.Next(), r.Next(), r.Next()));
            }
        }

        #region Using Framework Binary Formatter Serialization

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Coords", this.Coords);
        }

        internal Coordinates(SerializationInfo info, StreamingContext context)
        {
            this.Coords = (List<CoOrd>)info.GetValue("Coords", typeof(List<CoOrd>));
        }

        #endregion

        # region 'Raw' Binary Writer serialization

        public MemoryStream RawSerializeToStream()
        {
            MemoryStream stream = new MemoryStream(Coords.Count * 3 * 4 + 4);
            BinaryWriter writer = new BinaryWriter(stream);
            writer.Write(Coords.Count);
            foreach (CoOrd point in Coords)
            {
                writer.Write(point.x);
                writer.Write(point.y);
                writer.Write(point.z);
            }
            return stream;
        }

        public Coordinates(MemoryStream stream)
        {
            using (BinaryReader reader = new BinaryReader(stream))
            {
                int count = reader.ReadInt32();
                Coords = new List<CoOrd>(count);
                for (int i = 0; i < count; i++)                
                {
                    Coords.Add(new CoOrd(reader.ReadInt32(),reader.ReadInt32(),reader.ReadInt32()));
                }
            }        
        }
        #endregion
    }

    [TestClass]
    public class SerializationTest
    {
        [TestMethod]
        public void TestBinaryFormatter()
        {
            Coordinates c = new Coordinates();
            c.SetupTestArray();

            // Serialize to memory stream
            MemoryStream mStream = new MemoryStream();
            BinaryFormatter bformatter = new BinaryFormatter();
            bformatter.Serialize(mStream, c);
            Console.WriteLine("Length : {0}", mStream.Length);

            // Now Deserialize
            mStream.Position = 0;
            Coordinates c2 = (Coordinates)bformatter.Deserialize(mStream);
            Console.Write(c2.Coords.Count);

            mStream.Close();
        }

        [TestMethod]
        public void TestBinaryWriter()
        {
            Coordinates c = new Coordinates();
            c.SetupTestArray();

            MemoryStream mStream = c.RawSerializeToStream();
            Console.WriteLine("Length : {0}", mStream.Length);

            // Now Deserialize
            mStream.Position = 0;
            Coordinates c2 = new Coordinates(mStream);
            Console.Write(c2.Coords.Count);
        }

        [TestMethod]
        public void TestProtoBufV2()
        {
            Coordinates c = new Coordinates();
            c.SetupTestArray();

            MemoryStream mStream = new MemoryStream();
            ProtoBuf.Serializer.Serialize(mStream,c);
            Console.WriteLine("Length : {0}", mStream.Length);

            mStream.Position = 0;
            Coordinates c2 = ProtoBuf.Serializer.Deserialize<Coordinates>(mStream);
            Console.Write(c2.Coords.Count);
        }
    }
}

结果(注意 PB v2.0.0.423 beta)

                Serialize | Ser + Deserialize    | Size
-----------------------------------------------------------          
BinaryFormatter    2.89s  |      26.00s !!!      | 21.0 MB
ProtoBuf v2        0.52s  |       0.83s          | 18.7 MB
Raw BinaryWriter   0.27s  |       0.36s          | 11.4 MB

显然这只是看速度/大小,并没有考虑其他任何因素。

【问题讨论】:

  • @Ryan, this answer 建议使用protobuf-net 进行快速序列化。
  • @Ryan 和 protobuf-net "v2" 支持结构。让我稍后再看看(目前不是在 PC 上),但这是一个确定的选择。
  • 二进制序列化使用反射。这很慢,但从来都不是真正的问题,因为您将它用于 I/O。为什么你序列化到内存是不可猜测的。
  • 内存流 - 因为这将存储在 SQL 数据库而不是文件 sys 中(但不是 11MB,我使用过大的列表来强调性能问题)即使输出到文件同样的问题适用 - 二进制流如果这是原因,文件将不会消除反射的需要。
  • 为了澄清 18.7MB - 这被包括整个 Int32 范围所扭曲,其中 protobuf 默认情况下针对较小的数字进行了优化。如果您确实需要 2^31 范围,则可能首选使用固定宽度

标签: c# .net generics serialization


【解决方案1】:

使用BinaryFormatter 的二进制序列化在其生成的字节中包含类型信息。这会占用额外的空间。例如,在您不知道另一端期望什么数据结构的情况下,它很有用。

在您的情况下,您知道数据两端的格式是什么,这听起来不会改变。所以你可以编写一个简单的编码和解码方法。您的 CoOrd 类也不再需要可序列化。

我会使用 System.IO.BinaryReader 和 System.IO.BinaryWriter,然后循环遍历每个 CoOrd 实例并将 X、Y、Z 属性值读/写到流中。假设您的许多数字小于 0x7F 和 0x7FFF,这些类甚至会将您的 int 打包到不到 11MB。

类似这样的:

using (var writer = new BinaryWriter(stream)) {
    // write the number of items so we know how many to read out
    writer.Write(points.Count);
    // write three ints per point
    foreach (var point in points) {
        writer.Write(point.X);
        writer.Write(point.Y);
        writer.Write(point.Z);
    }
}

从流中读取:

List<CoOrd> points;
using (var reader = new BinaryReader(stream)) {
    var count = reader.ReadInt32();
    points = new List<CoOrd>(count);
    for (int i = 0; i < count; i++) {
        var x = reader.ReadInt32();
        var y = reader.ReadInt32();
        var z = reader.ReadInt32();
        points.Add(new CoOrd(x, y, z));
    }
}

【讨论】:

  • “二进制序列化在它生成的字节中包含类型信息” - 不,BinaryFormatter 做到了。二进制序列化一般没有这样的事情。
  • 对,是的,这就是我想要表达的观点。二进制序列化一般而言是一个概念,而不是一种技术。将编辑澄清。
【解决方案2】:

为了使用预构建序列化程序的简单性,我推荐protobuf-net;这是 protobuf-net v2,只添加了一些属性:

[DataContract]
public class Coordinates
{
    [DataContract]
    public struct CoOrd
    {
        public CoOrd(int x, int y, int z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        [DataMember(Order = 1)]
        int x;
        [DataMember(Order = 2)]
        int y;
        [DataMember(Order = 3)]
        int z;
    }
    [DataMember(Order = 1)]
    public List<CoOrd> Coords = new List<CoOrd>();

    public void SetupTestArray()
    {
        Random r = new Random(123456);
        List<CoOrd> coordinates = new List<CoOrd>();
        for (int i = 0; i < 1000000; i++)
        {
            Coords.Add(new CoOrd(r.Next(10000), r.Next(10000), r.Next(10000)));
        }
    }
}

使用:

ProtoBuf.Serializer.Serialize(mStream, c);

序列化。这需要 10,960,823 字节,但请注意,我调整了 SetupTestArray 以将大小限制为 10,000,因为默认情况下它对整数使用“varint”编码,这取决于大小。 10k 在这里并不重要(实际上我没有检查“步骤”是什么)。如果您更喜欢固定大小(允许任何范围):

        [ProtoMember(1, DataFormat = DataFormat.FixedSize)]
        int x;
        [ProtoMember(2, DataFormat = DataFormat.FixedSize)]
        int y;
        [ProtoMember(3, DataFormat = DataFormat.FixedSize)]
        int z;

这需要 16,998,640 字节

【讨论】:

  • 您列出属性 DataContract 和 DataMember - 它们应该是 ProtoContract 和 ProtoMember 还是我误解了? (有 PB v2.0.0.404)
  • @Ryan 没错;它试图适应。它将使用来自 [DataMember] 或来自 [XmlElement] 的 Order 来帮助从现有类型转换。特别是,从 LINQ 到 SQL。在 v2 中你甚至不需要属性(你可以单独告诉它绑定)
  • 反序列化也有问题 - 坐标 c2 = ProtoBuf.Serializer.Deserialize(mStream) - 使 c2.Coords 为空。我已将完整源代码放入 Q 编辑中。不得不承认没有给 RFM 足够的时间
  • @Ryan - 我无法重现该问题;我马上得到 100 万个结果。当然,我可能是你之前的版本......
猜你喜欢
  • 2017-05-27
  • 1970-01-01
  • 2010-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-28
相关资源
最近更新 更多