Не так давно я участвовал в оптимизации работы одного ASP.NET MVC веб-приложения. В частности, занимался улучшением производительности подсистемы кеширования. Сам проект использует Entity Framework 4.1, а означенная подсистема позволяет кешировать результаты отдельных LINQ to Entities запросов в memcached. Результат запроса ложится в кеш как массив сущностей (если точнее - массив экземпляров EF proxies) в сериализованном виде, при взятии из кеша его надо, соответственно, десериализовать.
Для (де)сериализации в приложении использовался старый добрый BinaryFormatter, производительность которого оставляла желать лучшего. Вообще говоря, приложение агрессивно использует кеш, поэтому медлительность BinaryFormatter-а была весьма заметна. Захотелось использовать более производительную библиотеку сериализации, желательно еще и генерирующую footprint поменьше. Да еще и такую, чтобы прикрутить было попроще. Поиск дал результаты, советующие взглянуть на protobuf-net, и в итоге мы стали использовать эту библиотеку в нашем проекте.
Краткое описание
protobuf-net это (наиболее живая) .NET реализация Protocol Buffers — кросс-платформенного протокола для обмена двоичными сообщениями от Google. Одна из особенностей этого протокола - уменьшение размера результирующего двоичного потока за счёт уменьшения избыточности кодирования (сжатия там нет, но вот трюки вроде Base 128 Varints есть) и уменьшения количества сериализуемой информации о типах (в контракте сериализуемого типа жёстко задаётся порядок полей, так что хранить имя поля и его тип нет необходимости).
В теории получается, что protobuf-net генерирует сериализованный поток меньшего размера и, значит, может (де)сериализовать быстрее, чем BinaryFormatter. Но для сериализации типа через protobuf-net контракт этого типа должен быть предварительно зарегистрирован — сериализовать любой [Serializable] класс «из коробки» protobuf-net не умеет.
Практика использования
protobuf-net можно или скачать с официального сайта, или поставить через NuGet. Классический пример использования protobuf-net выглядит примерно так:
У этого кода есть две особенности, которые могут сделать переход на protobuf-net неприятным:
- Для сериализуемого типа явно (с помощью атрибутов) указан контракт — какие поля/свойства необходимо сериализовать и в каком порядке. К сожалению, в существующем коде может быть много сериализуемых типов, и прописывать новые атрибуты для всех из них может быть тяжело/неудобно/невозможно.
- Необходимо явно указывать десериализуемый тип. Проблема тут в том, что на практике в момент десериализации он не всегда известен.
Добавления [Proto*] атрибутов к существующим классам можно избежать, задав контракт во внешнем .proto файле, используя атрибуты [DataContract]/[DataMember] или задавая контракт в рантайме через классы TypeModel/RuntimeTypeModel. Для существующих классов последний способ может оказаться наиболее полезным. Вот пример кода, который создаёт контракты для всех сериализуемых типов из текущей сборки:
Конечно, это только пример, и этот код придётся адаптировать под свои нужды — например, сериализовать все public properties или выбирать типы по пространству имён. А вот для проблемы десериализации неизвестного типа есть «официальное» решение — сначала сериализовать имя типа, а потом уже сам объект; соответствующий код можно подсмотреть в файле ProtoTranscoder.cs из SVN-репозитория protobuf-net.
Производительность
Для нашего проекта использование protobuf-net улучшило производительность (де)сериализации примерно в 2.5-3 раза (уменьшение размера сериализованного потока было для нас не так важно) — очень хороший результат, учитывая малые затраты на изменение кода. Вообще в интернетах есть несколько синтетических тестов с участием protobuf-net.
Интереса ради я написал и свой тест для сравнения protobuf-net с BinaryFormatter, код которого можно забрать на GitHub. В моём тесте:
- Сериализуется граф из 90 объектов (сущности 3-х типов).
- Граф содержит циклические ссылки.
- Для protobuf-net сериализуется информация о корневом типе.
Результаты теста, скомпилированного в Release, на моей машине:
Особенности и подводные камни
Используя protobuf-net, надо помнить о некоторых особенностях Protocol Buffers:
- Protocol Buffers разработан для сериализации деревьев объектов т.е. не поддерживает циклические ссылки и при сериализации сохраняет объект, на который есть ссылки из нескольких полей, как несколько независимых объектов. protobuf-net позволяет обходить эти ограничения и хранить объекты по ссылке, для этого нужно указать AsReference = true в метаданных сериализуемого поля.
- Protocol Buffers должен заранее знать о типах всех сериализуемых объектов и наследованиях между ними — эта информация должна явно указываться в контракте. Опять же, protobuf-net позволяет включить сериализацию информации о типе для поля, если в метаданных поля указать DynamicType = true.
- protobuf-net специальным образом обрабатывает прокси-типы, генерируемые NHibernate и Entity Framework-ом — если контракт прокси-типа не зарегистрирован, используется контракт его базового типа.
- Можно контролировать информацию о типе, сериализуемую для DynamicType полей, обрабатывая событие TypeModel.DynamicTypeFormatting. Например, в проекте, над которым я работал, этот обработчик использовался для регистрации контрактов неизвестных типов и сериализации EF прокси-объектов как объектов базового типа.
Это то, с чем я лично столкнулся, наверняка есть и другие детали работы библиотеки. Кроме того, в protobuf-net есть и баги — например, пустой массив может десериализоваться как null (я специальным образом обрабатываю этот случай в коде).
С документацией по protobuf-net дела обстоят не супер — хороших статей я не нашёл, пользовался такими источниками информации:
- Вопросы на StackOverflow — главный источник информации, много полезного, часто отвечает Marc Gravell (автор библиотеки).
- Блог Marc Gravell-а — информация общего плана.
- Официальный wiki — довольно неинформативен.
- Пост на Хабре — описывает в т.ч. использование protobuf-net в WCF.
И, конечно, очень помог код, потому что open source.
Здравствуйте,
ОтветитьУдалить"и уменьшения количества сериализуемой информации о типах (в контракте сериализуемого типа жёстко задаётся порядок полей, так что хранить имя поля и его тип нет необходимости)."
Объясните пожалуйста, разве Protobuf не хранит в информацию о типе, номер тега? Судя по тому, что написано в официально документации, например число 150 формата int c номером тэга 1 кодируется как:
08 96 01
где 08 это 0 0001 000 (первый бит MSB, 0001 - Tag, 000 - Type)
а 96 01 это само значение.