2011-12-22

Расширение возможностей LINQ to Objects

В проектах, которые активно используют LINQ вообще и LINQ to Objects в частности (т.е. практически во всех проектах на .NET 3.5+), часто можно увидеть подобный код:

Такие самописные методы-расширения появляются потому, что в стандартной поставке LINQ to Objects (System.Linq) недостаточно методов для решения всех типовых задач, и ForEach() — только один из примеров. Другой пример — методы получения элемента, в котором заданный член минимален/максимален (стандартные методы Min()/Max() возвращают сам минимальный/максимальный элемент).

Как результат, многие проекты содержат один и тот же набор велосипедов-расширений для LINQ. Естественно предположить, что раз многим нужен ForEach() для IEnumerable, то где-то должна быть библиотека, в котором этот и другие методы уже реализованы. Я нашёл несколько таких библиотек:

  • MoreLINQ — содержит только методы-расширения для IEnumerable общего назначения. Есть в NuGet.
  • LinqLib — содержит также методы для арифметических операций (например, сложить элементы двух последовательностей и получить третью) и методы для работы с числами Фибоначчи/простыми числами. Есть в NuGet.
  • Cadenza — в ней намешано много всего, в том числе и расширения для IEnumerable.

Все эти библиотеки имеют схожую функциональность для работы с IEnumerable (например, во всех из них есть метод-расширение ForEach), так что выделить какую-то одну из них тяжело. Лично мне интерфейс библиотеки MoreLINQ показался достаточно грамотно спроектированным (ничего лишнего), плюс она есть в NuGet; в общем случае выбор придётся делать, отталкиваясь от нужд конкретного проекта.

2011-12-14

Expression Trees: Трансформация переменных в константы

Работая над кешированием LINQ to Entities запросов (см. пост про protobuf-net) я столкнулся с проблемой вычисления уникального ключа запроса. В рамках этой проблемы необходимо было решить две задачи, связанные с обработкой LambdaExpression-ов в IQueryable, для которого генерируется ключ:

  1. Для одинаковых лямбда-выражений, зависящих от переменной с разными значениями, должны генерироваться разные ключи.
  2. Для разных лямбда-выражений, зависящих от переменных с одинаковыми значениями, должны генерироваться одинаковые ключи.

Эти задачи можно сформулировать в виде кода:

Я решил обе задачи с помощью ExpressionVisitor-а, который рекурсивно проходит по дереву выражений и заменяет любое выражение, все дочерные узлы которого являются константами, на константу. Вот код моего решения:

Этот код «отрезает» лямбда-выражение от контекста, в котором оно было создано, и превращает все замкнутые в выражении переменные в константы. Кроме генерации уникального ключа по выражению приведённый выше код можно, например, использовать в своём Query Provider-е для примитивной нормализации лямбда-выражений и получения значений всех параметров запроса.

2011-12-12

protobuf-net на практике

Не так давно я участвовал в оптимизации работы одного 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 неприятным:

  1. Для сериализуемого типа явно (с помощью атрибутов) указан контракт — какие поля/свойства необходимо сериализовать и в каком порядке. К сожалению, в существующем коде может быть много сериализуемых типов, и прописывать новые атрибуты для всех из них может быть тяжело/неудобно/невозможно.
  2. Необходимо явно указывать десериализуемый тип. Проблема тут в том, что на практике в момент десериализации он не всегда известен.

Добавления [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 дела обстоят не супер — хороших статей я не нашёл, пользовался такими источниками информации:

И, конечно, очень помог код, потому что open source.