Улучшение производительности двоичной сериализации для большого списка структур
У меня есть структура, содержащая 3d-координаты в 3 интервалах. В тесте я собрал List < > из 1 миллиона случайных точек, а затем использовал двоичную сериализацию в потоке памяти.
Поток памяти идет в ~ 21 МБ, что кажется очень неэффективным, как 1000000 точек * 3 коорда * 4 байта должны выходить с минимальным значением 11 МБ
Его также занимает ~ 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
Очевидно, это просто смотрит на скорость/размер и не учитывает ничего другого.
Ответы
Ответ 1
Двоичная сериализация с использованием BinaryFormatter
включает информацию о типе в байтах, которые она генерирует. Это занимает дополнительное пространство. Это полезно в тех случаях, когда вы не знаете, какую структуру данных ожидать на другом конце, например.
В вашем случае вы знаете, какой формат данных имеет на обоих концах, и это не похоже на то, что он изменится. Таким образом, вы можете написать простой метод кодирования и декодирования. Ваш класс CoOrd больше не нуждается в сериализации.
Я бы использовал System.IO.BinaryReader и System.IO.BinaryWriter, затем прокручивал каждый из ваших экземпляров CoOrd и читал/записывал X, Y, Z для потока. Эти классы даже упакуют ваши ints менее чем на 11 МБ, если многие из ваших чисел меньше 0x7F и 0x7FFF.
Что-то вроде этого:
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));
}
}
Ответ 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 байт