.NET

индекс
121,03

Benchmark: AutoMapper vs BLToolkit vs EmitMapper

О чем речь?


Речь в этой заметке пойдет о библиотеках для автоматического копирования полей одного объекта в поля другого (мэппинг объектов). О том, для чего это надо можно почитать, например, тут.

Предлагаю рассмотреть следующие библиотеки, с помощью которых можно решить вышеописанную задачу:

1) AutoMapper
2) BLToolkit
3) EmitMapper

Все эти библиотеки отличает тот факт, что они используют Emit для генерации кода во время выполнения, и потенциально могут работать с эффективностью приближенной к ручному кодированию. Так ли это, предлагаю проверить.

AutoMapper

Сайт: automapper.codeplex.com/

На данный момент это самая распространенная библиотека. Библиотека содержит приличный набор медодов для расширения и кастомизации мэппинга, а также поддерживает работу со сложными вложенными объектами. Хотя для работы с последними придется дополнительно сделать «приседания», предварительно зарегистрировав каждую пару сопоставляемых типов.

BLToolkit

Сайт: bltoolkit.net/

Это не чистый Objects to Objects мэппер (скорее это легковесная ORM), хотя и поддерживает мэппинг одних объектов в другие. Но из-за того, что это не чистый Objects to Objects мэппер у него отсутствуют многе возможности присущие полноценным мэпперам таким как AutoMapper или EmitMapper. Библиотека не поддерживает сложные объекты, да и сам мэппинг не может быть каким-либо способом кастомизирован.

EmitMapper

Сайт: emitmapper.codeplex.com/

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

Сравнение производительности.


Для сравнение производительности были написаны следующие тесты.

Простой тест.

Сопоставляются два простых класса, все поля которых имеют примитивные типы и без какой-либо дополнительной кастомизации. Это единственный тест, который может быть выполнен на BLToolkit. Код теста можно найти здесь.

Результаты (время работы в миллисекундах):
Auto Mapper 36809
BLToolkit 34533
Emit Mapper 117
Handwritten Mapper 37

По цифрам видно, что разница очень впечатляющая, но для лучшего представления масштаба предлагаю посмотреть на диаграмму:

simple test

Вложенные классы.

Сопоставляются два класса, имеющие сложную структуру. Код теста можно посмотреть здесь.

Результаты (время работы в миллисекундах):
Auto Mapper 52238
Emit Mapper 102
Handwritten Mapper 97
BLToolkit не поддерживается

В данном тесте видно, что производительность Emit Mapper и ручного кода практически идентична.
Диаграмма:
nested classes

Кастомизация.

В данном тесте проверяется скорость работы сложного кастомизированного сопоставления. В частности активно используются Custom constructors, Null substitution и Custom converters. Код теста доступен здесь

Результаты (время работы в миллисекундах):
Auto Mapper 50142
Emit Mapper 197
BLToolkit не поддерживается

Диграмма:
customized mapping

Выводы


Продемонстрированная разница в производительности является весьма существенной и меняет сам подход к использования автоматических объектых мэпперов. Если в случае с AutoMapper мы должны постоянно держать в уме требования к данному участку кода (как часто он используется), то в случае с EmitMapper ничего этого не требуется. Мы просто выполняем мэппинг там, где нам нужно и знаем, что с производительностью будет все нормально.

P.S.: В комментариях резонно спросили о причинах такой разницы в производительности. Чтобы избежать недоразумений, вот они:
1) В EmitMapper есть возможность заранее сгенерировать мэппер и где-то его сохранить (например, во время старта программы в статическом поле). В AutoMapper и BLToolkit мы вынуждены использовать одну единственную глобальную точку входа типа: «Map.ObjectToObject(foo)». А это затраты на синхронизацию, поиск по словарю и т.д.
2) AutoMapper был изначально спроектирован и написан для Reflection и лишь потом переведен на Emit. Из-за этого там осталось очень много ненужного оверхеда.
3) EmitMapper практически нигде не использует boxing/unboxing в отличии от его конкурентов. Да и вообще, код, который он генерирует весьма эффективен.
+12
14 января 2010, 19:36
25

комментарии (20)

+1
sha1dy #
спасибо! как раз стоял выбор между подобными библиотеками, теперь очевидно — EmitMapper, да и его fluent интерфейс (после fluentHibernate, Moq и Autofaq) очень и очень приятен.
0
scorzh #
Спасибо за наводку на EmitMapper и за сравнение производительности. Попробую в своем проекте заменить AutoMapper на EmitMapper. Было бы здорово, если бы еще описали, какие именно особенности реализации дали такую разницу производительности.
+1
ostapbender #
Опять какая-то синтетика, да еще и с неправильными замерами. Уж сколько ж раз повторяли, что JIT компилирует каждый метод отдельно, так что для получения хоть сколько-нибудь корректных данных надо каждый метод выполнить отдельно и без замеров производительности.
+1
icoder #
Время работы тестов по 30-50 секунд. Сами тесты из 5 строк, во всех текст самих тестов практически одинаковый. Так что временем работы JIT'a можно пренебречь
0
ostapbender #
А попробовать?
0
icoder #
Проверил. Перед каждым тестом добавил холодный прогон и GC.Collect(), чтобы все находились в равных условиях. Вот результаты:

Auto Mapper (simple): 37652 milliseconds
BLToolkit (simple): 34886 milliseconds
Emit Mapper (simple): 113 milliseconds
Handwritten Mapper (simple): 37 milliseconds
Auto Mapper (Nested): 52456 milliseconds
Emit Mapper (Nested): 91 milliseconds
Handwritten Mapper (Nested): 86 milliseconds
Auto Mapper (Custom): 49348 milliseconds
Emit Mapper (Custom): 188 milliseconds

Вобщем, они практически не отличаются от тех, что в заметке.
+2
icoder #
Из особенностей, которые так сильно повлияли на производительность самые главные это:
1) В EmitMapper есть возможность заранее сгенерировать мэппер и где-то его сохранить (например, во время старта программы в статическом поле). В AutoMapper и BLToolkit мы вынуждены использовать одну единственную глобальную точку входа типа: «Map.ObjectToObject(foo)». А это затраты на синхронизацию, поиск по словарю и т.д.
2) AutoMapper был изначально спроектирован и написан для Reflection и лишь потом переведен на Emit. Из-за этого там осталось очень много ненужного оверхеда.
3) EmitMapper практически нигде не использует boxing/unboxing в отличии от его конкурентов
+2
centur #
Каждый кулик свое болото хвалит ;)

http://rsdn.ru/forum/dotnet/3658986.1.aspx
0
chaliy #
В EmitMapper есть возможность заранее сгенерировать мэппер и где-то его сохранить (например, во время старта программы в статическом поле). В AutoMapper и BLToolkit мы вынуждены использовать одну единственную глобальную точку входа типа: «Map.ObjectToObject(foo)». А это затраты на синхронизацию, поиск по словарю и т.д.


Это вранье… «Map.ObjectToObject(foo)» это просто один из вариантов использования. Посмотрите на MappingEngine. Свободно кешиться в синглтоне. Для мебя мы вообще сделали хелперы, и забыли.
0
chaliy #
Простите, это было про AutoMapper, но быстрее всего если вы глубже копнете, тока окажеться что и в BLTollkit есть что-то подобное.
0
icoder #
Я основывался на бенчмарке автора AutoMapper
code.google.com/p/automapperhome/source/browse/trunk/src/Benchmark/FlatteningMapper.cs
Вот кусок кода оттуда:
public void Map()
{
Mapper.Map<ModelObject, ModelDto>(_source);
}

Я полагаю, что автору виднее, как правильно использовать его собственную библиотеку.
0
chaliy #
Я полагаю, что щас пришлю вам патч, а потом посмотрим.
0
icoder #
Сам автор BlToolkit подтвердил, что такого интерфейса нет.
0
icoder #
Не нашел в MappingEngine ничего подобного. Вот его код: code.google.com/p/automapperhome/source/browse/trunk/src/AutoMapper/MappingEngine.cs

Можете носом ткнуть?
0
chaliy #
ессно

var configuration = new Configuration(new TypeMapFactory(),
	MapperRegistry.AllMappers.Invoke());
configuration.CreateMap<B2, A2>();
configuration.CreateMap<char, int>();
autoMapper = new MappingEngine(configuration);

я им когда-то сабмитил патч, тобы попроще выглядело, но что есть то есть.

и использование

d = autoMapper.Map(s, d);


Проблема в том что мы не всерано придеться посыпать голову пеплом. При том что про Map.ObjectToObject это действительно вранье. Кеширование конфигруции практически никчему не приводит. Как то так.

Простите. Я просто должен был указать на неточность не более того.
0
icoder #
Во-первых, я не понял, в чем заключается «вранье» про «Map.ObjectToObject»?

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

В-третьих у MappingEngine нет метода «Map», который принимает два объекта. Назовите мне номер строки с этим методом здесь:
code.google.com/p/automapperhome/source/browse/trunk/src/AutoMapper/MappingEngine.cs
0
icoder #
Метод Map с двумя параметрами нашел, но это не сгенерированный маппер.
public TDestination Map<TSource, TDestination>(ResolutionContext parentContext, TSource source)
{
Type destinationType = typeof(TDestination);
Type sourceType = typeof(TSource);

}

Это не именни никакого отношение к тому, о чем я говорил
0
chaliy #
А как можно получить тестовый код, так что бы я мог написать «правильный» код для AutoMapper?
0
icoder #
Все ссылки есть в статье. Читайте внимательнее.
0
Denisio #
EmitMapper действительно быстр, попробовал сейчас вместо BLToolkitовых запросов типовые операции прогнать (10-12 полей разных типов — DateTime/string/bool/int/double) на 200-500 элементах коллекций — EmitMapper действительно сильно быстрее.

Пока есть два замечания:
1) атрибутная фильтрация немаппирумых полей — в классе есть, в запросе нет. Сейчас падает с исключением, но вообще по здравому размышлению — наверно так оно и правильней — подумать надо.
2) атрибутная установка PK и название таблицы, чтобы не приходилось каждый раз передавать в UpdateObject (файл DBTools.cs) — надо добавить, тут несложно — сам сделаю.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.