Как выжать максимум производительности из probuf-net

Я хочу использовать protobuf-net для сериализации данных фондового рынка. Я играю со следующей моделью сообщения:

1st message: Meta Data describing what data to expect and some other info.
2nd message: DataBegin
3rd message: DataItem
4th message: DataItem
...
nth message: EndData

Вот пример элемента данных:

class Bar{
   DateTime DateTime{get;set;}
   float Open{get;set}
   float High{get;set}
   float Low{get;set}
   float Close{get;set}
   intVolume{get;set}
 }

Прямо сейчас я использую TypeModel.SerializeWithLengthPrefix (...) для сериализации каждого сообщения (TypeModel скомпилирован). Что отлично работает, но это примерно в 10 раз медленнее, чем сериализация каждого сообщения вручную с помощью BinaryWriter. Конечно, здесь важны не метаданные, а сериализация каждого DataItem. У меня много данных, и в некоторых случаях они читаются / записываются в файл, поэтому производительность имеет решающее значение.

Что было бы хорошим способом повышения производительности сериализации и десериализации каждого элемента данных?

Должен ли я использовать ProtoWriter прямо здесь? Если да, то как мне это сделать (я немного новичок в протоколах буферов).


person lukebuehler    schedule 30.11.2011    source источник
comment
Самый быстрый способ сделать это - записать последовательность как одно сообщение, используя группы для вложенных сообщений. Если я попробую несколько вещей, о скольких значениях DataItem мы обычно говорим? (просто чтобы я мог представить реалистичный случай).   -  person Marc Gravell    schedule 30.11.2011
comment
От 200 до 100 000 наименований. Но пока допустим 100000 сообщений. Но тогда может быть до 6000 потоков данных по 100000 сообщений в каждом. Это в основном для бэк-тестирования, поэтому речь идет о том, как быстро я могу записывать сообщения в файлы, а затем загружать их. При использовании в реальном времени производительность не так важна.   -  person lukebuehler    schedule 30.11.2011
comment
Мне кажется, я не могу полностью написать поток вручную, чтобы имитировать SerializeWithLengthPrefix. Как мне написать префикс 0A (ключ равен 1) в начале двоичного потока с помощью ProtoWriter?   -  person lukebuehler    schedule 01.12.2011
comment
честно говоря, не переходите на ProtoWriter - это не будет ключевым отличием (это то, что CompileInPlace уже делает)   -  person Marc Gravell    schedule 01.12.2011


Ответы (1)


Да, если ваши данные представляют собой очень простой набор однородных записей без дополнительных требований (например, они не должны быть совместимы с другими версиями, элегантно версия или использоваться клиентами, которые не полностью знают все данные) , не обязательно должно быть удобно переносимым, и вы не против реализовать всю сериализацию вручную, тогда да: вы можете сделать это более эффективно вручную. В быстром тесте:

protobuf-net serialize: 55ms, 3581680 bytes
protobuf-net deserialize: 65ms, 100000 items
BinaryFormatter serialize: 443ms, 4200629 bytes
BinaryFormatter deserialize: 745ms, 100000 items
manual serialize: 26ms, 2800004 bytes
manual deserialize: 32ms, 100000 items

Дополнительное пространство - это, предположительно, маркеры полей (которые вам не нужны, если вы упаковываете записи вручную и вам не нужно беспокоиться о разных версиях API, используемых одновременно).

Я, конечно, не воспроизводю "10x"; Я получаю 2x, что неплохо, учитывая то, что предлагает protobuf. И, безусловно, намного лучше, чем BinaryFormatter, что больше похоже на 20x! Вот некоторые функции:

  • допуск к версии
  • переносимость
  • использование схемы
  • без ручного кода
  • встроенная поддержка подобъектов и коллекций
  • поддержка исключения значений по умолчанию
  • поддержка распространенных сценариев .NET (обратные вызовы сериализации; шаблоны условной сериализации и т. д.)
  • наследование (только protobuf-net; не является частью стандартной спецификации protobuf)

Это звучит, как будто в вашем сценарии ручная сериализация - это то, что нужно сделать; это нормально - я не обижен; цель библиотеки сериализации - решить более общую проблему таким образом, чтобы не требовалось писать код вручную.

Моя испытательная установка:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using ProtoBuf;
using ProtoBuf.Meta;
using System.Runtime.Serialization.Formatters.Binary;

public static class Program
{
    static void Main()
    {

        var model = RuntimeTypeModel.Create();
        model.Add(typeof(BarWrapper), true);
        model.Add(typeof(Bar), true);
        model.CompileInPlace();

        var data = CreateBar(100000).ToList();
        RunTest(model, data);

    }

    private static void RunTest(RuntimeTypeModel model, List<Bar> data)
    {
        using(var ms = new MemoryStream())
        {
            var watch = Stopwatch.StartNew();
            model.Serialize(ms, new BarWrapper {Bars = data});
            watch.Stop();
            Console.WriteLine("protobuf-net serialize: {0}ms, {1} bytes", watch.ElapsedMilliseconds, ms.Length);

            ms.Position = 0;
            watch = Stopwatch.StartNew();
            var bars = ((BarWrapper) model.Deserialize(ms, null, typeof (BarWrapper))).Bars;
            watch.Stop();
            Console.WriteLine("protobuf-net deserialize: {0}ms, {1} items", watch.ElapsedMilliseconds, bars.Count);
        }
        using (var ms = new MemoryStream())
        {
            var bf = new BinaryFormatter();
            var watch = Stopwatch.StartNew();
            bf.Serialize(ms, new BarWrapper { Bars = data });
            watch.Stop();
            Console.WriteLine("BinaryFormatter serialize: {0}ms, {1} bytes", watch.ElapsedMilliseconds, ms.Length);

            ms.Position = 0;
            watch = Stopwatch.StartNew();
            var bars = ((BarWrapper)bf.Deserialize(ms)).Bars;
            watch.Stop();
            Console.WriteLine("BinaryFormatter deserialize: {0}ms, {1} items", watch.ElapsedMilliseconds, bars.Count);
        }
        byte[] raw;
        using (var ms = new MemoryStream())
        {
            var watch = Stopwatch.StartNew();
            WriteBars(ms, data);
            watch.Stop();
            raw = ms.ToArray();
            Console.WriteLine("manual serialize: {0}ms, {1} bytes", watch.ElapsedMilliseconds, raw.Length);
        }
        using(var ms = new MemoryStream(raw))
        {
            var watch = Stopwatch.StartNew();
            var bars = ReadBars(ms);
            watch.Stop();
            Console.WriteLine("manual deserialize: {0}ms, {1} items", watch.ElapsedMilliseconds, bars.Count);            
        }

    }
    static IList<Bar> ReadBars(Stream stream)
    {
        using(var reader = new BinaryReader(stream))
        {
            int count = reader.ReadInt32();
            var bars = new List<Bar>(count);
            while(count-- > 0)
            {
                var bar = new Bar();
                bar.DateTime = DateTime.FromBinary(reader.ReadInt64());
                bar.Open = reader.ReadInt32();
                bar.High = reader.ReadInt32();
                bar.Low = reader.ReadInt32();
                bar.Close = reader.ReadInt32();
                bar.Volume = reader.ReadInt32();
                bars.Add(bar);
            }
            return bars;
        }
    }
    static void WriteBars(Stream stream, IList<Bar> bars )
    {
        using(var writer = new BinaryWriter(stream))
        {
            writer.Write(bars.Count);
            foreach (var bar in bars)
            {
                writer.Write(bar.DateTime.ToBinary());
                writer.Write(bar.Open);
                writer.Write(bar.High);
                writer.Write(bar.Low);
                writer.Write(bar.Close);
                writer.Write(bar.Volume);
            }
        }

    }
    static IEnumerable<Bar> CreateBar(int count)
    {
        var rand = new Random(12345);
        while(count-- > 0)
        {
            var bar = new Bar();
            bar.DateTime = new DateTime(
                rand.Next(2008,2011), rand.Next(1,13), rand.Next(1, 29),
                rand.Next(0,24), rand.Next(0,60), rand.Next(0,60));
            bar.Open = (float) rand.NextDouble();
            bar.High = (float)rand.NextDouble();
            bar.Low = (float)rand.NextDouble();
            bar.Close = (float)rand.NextDouble();
            bar.Volume = rand.Next(-50000, 50000);
            yield return bar;
        }
    }

}
[ProtoContract]
[Serializable] // just for BinaryFormatter test
public class BarWrapper
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public List<Bar> Bars { get; set; } 
}
[ProtoContract]
[Serializable] // just for BinaryFormatter test
public class Bar
{
    [ProtoMember(1)]
    public DateTime DateTime { get; set; }

    [ProtoMember(2)]
    public float Open { get; set; }

    [ProtoMember(3)]
    public float High { get; set; }

    [ProtoMember(4)]
    public float Low { get; set; }

    [ProtoMember(5)]
    public float Close { get; set; }

    // use zigzag if it can be -ve/+ve, or default if non-negative only
    [ProtoMember(6, DataFormat = DataFormat.ZigZag)]
    public int Volume { get; set; }
}
person Marc Gravell    schedule 30.11.2011
comment
Вау, спасибо за подробный ответ. Чтобы объяснить немного больше, как я хочу использовать буферы протокола. Я хочу использовать обычный способ se (de) rializing почти всех сообщений, используя typeModel.Serialize или что-то еще. В основном из-за всех перечисленных вами преимуществ. НО в некоторых случаях для определенных типов данных (например, Bars) id хотел бы переключиться в режим ручной сериализации, но я хочу, чтобы данные по-прежнему были двоично совместимыми, например сериализовать с помощью TypeModel десериализовать вручную. Дополнительная работа для тех немногих типов, где я бы это сделал, будет нормой, если я выжму из нее некоторую скорость. - person lukebuehler; 01.12.2011
comment
в десериализации вручную очень мало смысла ... вы просто дублируете то, что уже делает Compile () ... единственный способ сделать это более жестким - не использовать формат проводов protobuf (как определено google). Вы могли использовать несколько byte[] BLOB местами? Кроме этого ... или я упускаю из виду то, что вы пытаетесь сделать? - person Marc Gravell; 01.12.2011
comment
Да, ты прав, теперь я тоже понимаю. С некоторыми настройками я теперь в 3 раза медленнее, чем ручная сериализация. Но я вижу, что это не может быть намного ближе, чем это (просто нужно просто написать каждый заголовок). Спасибо за вашу помощь! Я очень люблю Protobuf-net! Я должен посмотреть, приемлемо ли x2-x3 или нет. Но это, безусловно, самый быстрый сериализатор, который я когда-либо видел! - person lukebuehler; 01.12.2011
comment
@lukebuehler обратите внимание, что обычно io / bandwidth является ограничивающим фактором. Запись в поток памяти может немного повлиять на числа. Попробуй сделать и диск. Затем добавьте SSD: обратите внимание на другие уловки, которые я применил там: внешний объект-оболочку и групповое кодирование для элементов списка. - person Marc Gravell; 01.12.2011