Monday, November 10, 2008

Сравнение производительности .NET ORM: Часть 1. Выборки данных

Сегодня я хотел бы начать серию заметок о сравнении производительности Entity Framework, LINQ to SQL, Active Record (NHibernate) и классического ADO.NET. Я еще не знаю, во что выльется эта идея, и к чему мы придем в результате, но основными целями этого сравнения я бы назвал:

  1. получение адекватных результатов сравнения и попытка их объяснить
  2. обсуждения различных вариантов увеличения производительности вышеуказанных способов доступа к базе данных
  3. получение понимания, какие варианты доступа к базе данных являются наиболее приемлемыми в той или иной ситуации с точки зрения производительности

Сразу скажу, что, несмотря на то, что я уже почти год использую Entity Framework, я не буду стараться его выгораживать. Откровенно говоря, сейчас мы в нашем проекте подошли к той границе, за которой bottleneck начинается уже в самом Entity Framework, а я пока и близко не в восторге от производительности нашей системы. Поэтому я постараюсь быть максимально объективным в данном вопросе, а если все же где-нибудь буду говорить чушь, вы меня поправите.

Изначально я хотел выяснить производительность EF и L2S, а также сравнить их с классическим ADO.NET, т.к. последний на данный момент является наиболее быстрым способом доступа к базе данных. Естественно, любая ORM будет медленнее. Потом меня эта идея настолько увлекла, что я для полноты картины добавил еще и Active Record, который, как известно, базируется на NHibernate. Еще один важный момент: я вижу NHibernate впервые в жизни, поэтому: а) не судите строго тот код, который я с горем пополам в нем написал, б) если у вас есть советы и комментарии, как по коду, так и по улучшению производительности – буду счастлив услышать :)

Все, довольно вступлений. Итак, для начала я покажу результаты моих тестов read-запросов. В следующих заметках серии я постараюсь уделить время генерируемым запросам, а также CUD-операциям. Пока же ограничимся выборками.

Используемые тесты

Использовал я всеми любимую базу Northwind, в частности таблицы Customers, Orders, Order Details и Products. Для каждого из способов доступа к данным я сделал 8 простых тестов:

Тест Описание Цель
Инициализация Создание контекста, инициализация, чтение строки подключения Получить время инициализации, возможно, оптимизировать его
Получение всех заказов Простая выборка всех строк из таблицы Orders Самый простой запрос, в то же время можно сравнить время материализации, т.к. там больше 800 записей
Многократное получение всех заказов Выборка всех строк из таблицы Orders 3 раза Сравнить степень кеширования запросов и их результатов
Получение всех продуктов одного заказчика Один сложный запрос через 3 таблицы Сравнить эффективность генерации SQL-скриптов, материализация минимальна
Многократное получение всех продуктов одного заказчика Тот же запрос 3 раза То же самое + кеширование
Получение всех продуктов одного заказчика (сложная версия) Пошаговое получение того же результата, что и в предыдущем случае. Берем коллекцию, проходимся foreach'ем, берем связанные записи и т.д. Сравнить данный способ обращения к данным с предыдущим
Многократное получение всех продуктов одного заказчика (сложная версия) То же самое 3 раза То же самое + кеширование
Микс: получение заказов + продуктов заказчика Синтетический тест, эмулирующий разные запросы Посмотреть, как влияют результаты выполнения одного запроса на следующий
Тест более серьезной нагрузкой Все предыдущие запросы последовательно за один присест Попробовать нагрузить базу большим количеством последовательных запросов, и посмотреть, что получится

Код я здесь приводить не буду, а лучше дам вам ссылочку, где можно скачать приложение, чтобы глянуть запросы и код и надавать автору по голове за него посоветовать мне, что и как можно улучшить :) Для запуска приложения вам понадобится база Northwind, VS2008 SP1 (там есть EF и L2S), а также установленный Active Record (скачать можно здесь). Не забудьте сконфигурировать строки подключения к базе и параметры Active Record в классе ActiveRecordProvider (да, знаю, что за такое нужно отбивать руки, но для текущей задачи такой реализации с головой).

Пара слов перед тем, как показать результаты. Тестировал я достаточно просто, вручную запуская один тест за другим, но в то же время я старался учесть кеширование запросов в SQL Server'е. Не знаю, насколько мне это удалось, но я старался запускать каждый тест отдельно вразнобой, и с некоторыми временными интервалами. К сожалению, тесты с базой данных – это такая вещь, которую бывает трудно воспроизвести, результаты варьируются, но, надеюсь, показанные числа дадут хотя бы какое-то общее представление о порядках. В любом случае, вы можете скачать исходники, установить L2S, EF, AR (если еще не установлены) и поиграться сами. Машинка у меня дома относительно небыстрая (Athlon 64 3200+ с 3 гигами мозгов), поэтому делайте скидку и на это.

Анализ результатов

Итак, результаты в миллисекундах:

Test Classic L2S EF NH/AR
Initialization 15 78 101 410
Orders 125 172 1078 1062
OrdersMultiple 187 218 1093 1187
CustomerProducts 109 234 1328 828
CustomerProductsMultiple 125 281 1344 1156
CustomerProductsComplex 140 464 1453 937
CustomerProductsComplexMultiple 203 511 1515 1109
Mixed 140 296 1265 1187
All 187 515 1656 1546

Что сразу бросается в глаза, так это тотальное отставание EF и AR от L2S (в 3-6 раза), а также то, насколько быстро L2S работает с базой – в среднем всего лишь где-то в 2 раза медленнее. При ближайшем рассмотрении можно сделать вывод, что AR работает быстрее, чем EF, по крайней мере, в запросах, возвращающих небольшое количество данных. У меня есть подозрение, что AR медленнее материализует объекты, чем EF (запрос по Orders – самый простой, но в то же время самый большой по количеству данных), но это нужно взять выборку побольше, чтобы проверить. При этом инициализация контекста у EF происходить почти так же быстро, как и у L2S, а AR тут немного отстает, хотя это и не страшно. Что еще важно: все ORM отлично справляются с кешированием повторных запросов: 3 запроса выполняются ненамного дольше, чем один. Связано это, прежде всего, с тем, что они кешируют не только материализованные объекты, чем существенно экономят время, но и, в случае EF – деревья промежуточных запросов (CQT, Canonical Query Tree). Так что одинаковые и даже похожие запросы выполняются намного быстрее. Также показателен последний тест: он дает возможность увидеть, что каждый запускаемый поодиночке тест все-таки несет определенный накладные расходы, потому что один общий тест лишь очень ненамного превышает самый медленный из своих компонентов. А это говорит о том, что НЕ НУЖНО создавать контексты на каждый запрос (это касается, в первую очередь L2S и EF), как бы вас этому ни учили в умных статьях. Если хотите добиться хорошей производительности – старайтесь использовать контексты как можно дольше, но в разумных пределах. Мы вот у себя нашли этот самый разумный предел – контекст живет в пределах обработки одной страницы, то есть контекст создается на каждый HttpRequest и умирает вместе с ним. В сложных случаях многостраничных диалогов мы продлеваем время жизни контекста, перемещая его временно в сессию.

Теперь пара слов о том, почему же мы видим такие результаты. Ну, во-первых, L2S у нас завязан лишь на один SQL Server и, если я не ошибаюсь, не строит никаких промежуточных CQT, чтобы общаться с тысячей различных провайдеров, как это делает EF. Во-вторых, EF – это не просто ORM, способная работать с разными базами данных и на лету подменять их без изменения кода. EF также обладает различными продвинутыми фичами маппинга, которых как минимум нет в L2S (не буду говорить про NH, ибо не знаю всех его возможностей). В-третьих, судя по проанализированным мною сгенерированным SQL-запросам, здесь особой разницы между ORM нет (подробнее об этом мы поговорим в следующей заметке), поэтому сюда особо рыть не стоит. И, в-четвертых, EF еще только-только зарелизился, думаю, мы еще увидим более быстро работающие версии.

Советы по использованию подопытных

Итак, определенные советы по использованию этих ORM (в широком смысле, L2S – это тоже ORM) можно дать уже сейчас. Если вам нужно написать простенькое приложение – используйте L2S – это САМЫЙ быстрый способ. Накидали табличек в базу, сгенерировали модель 2-мя кликами мыши – и вуаля, можно работать. Для более серьезных приложений уже нужно смотреть внимательнее. С точки зрения скорости написания кода равных L2S и EF нет. При всем уважении, AR/NH пока медленнее за счет того, что нет тула по созданию и поддержке модели. Если вам нужны продвинутые возможности маппинга, вы делаете разработку в рамках DDD или нужна поддержка различных баз данных – смотрите в сторону AR/NH и EF. Тут уже что больше по душе. Если же у вас на первом месте производительность и вы работаете лишь с MS SQL Server – тогда вам, наоборот, нужно смотреть либо в сторону L2S, либо вообще в сторону классического ADO.NET. Однако, в случае с классическим ADO.NET время разработки, а также последующей поддержки Data Access Layer увеличивается не просто в разы, а на порядки. Да, и пусть вас не смущают результаты запросов секунда и выше – как можно заметить, реально занимают время первые запросы, а остальные идут очень быстро и по накатанной. Более того, могу сказать по опыту, что где-то для 50-60% приложений производительности EF и AR/NH должно хватить с головой. Так что, если у вас не rocket science, не стоит заморачиваться.

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

Надеюсь, что в свете того, что у нас теперь появляется все больше выбора, я смог пролить хотя бы немного света на производительность различных способов обращения к данным. Если у кого-то уже сейчас есть комментарии по улучшению производительности той или иной ORM – пожалуйста. Мои тесты покрывают лишь дефолтные настройки.

Полезные ссылки:

Тестовый проект для сравнения производительности L2S, EF и AR/NH между собой и с классическим ADO.NET

Exploring the Performance of the ADO.NET Entity Framework - Part 1
Exploring the Performance of the ADO.NET Entity Framework - Part 2
ADO.NET Entity Framework Performance Comparison
Entity Framework Instantiation Times with a 120-Table Database
Linq to SQL vs NHibernate Part 1: What do they have in common?

19 comments:

  1. Интересный пост и интересно дальнейшее сравнение. А почему Вы используетее Active Record написанную на основе nHibernate, а не собственно сам nHibernate?

    ReplyDelete
  2. Restuta: Здесь несколько причин. Во-первых, насколько я понял, прослойка AR достаточно тонка, поэтому производительность находится почти на том же уровне, что и у NH, что мне подходит для теста (знатоки, поправьте меня, если я не прав). В то же время мне давно хотелось посмотреть, что такое AR и как с ним работать. Плюс разработка на AR для человека, не очень знакомого с NH, проходит быстрее, а я не хотел сильно затягивать с тестом.

    Думаю, можно провести дополнительные тесты для NH, если кого-то это интересует.

    ReplyDelete
  3. Интересная статья было интересно узнать(хотя бы приблизительно) насколько orm проигрывает ado.net и l2s. Особенно было интересно про контекст (NH использовал ISession на запрос, а в EF насмотревшись обучалок на запрос - всё таки в дальнейшем лучше проверять) Но вот только предвзятое отношение к EF. Поработав несколько лет с NH решил попробовать EF в последнем проекте - увидел, что дизайнер полон кучи ошибок. Только в сценарии - создал бд и нажал кнопочку Update дизайнер может сгенерировать работающюю схему. А на практике оказалось что когда бд дорабатывается в ходе проекта, то дизайнер во множестве случаеев попросту не может переделать схему в результате приходилось открывать xml и в ручную всё исправлять. С другой стороны открываю NH проект, а там простые классы с атрибутами для мапинга, правится всё просто и быстро.
    Ну и насчёт дизайнеров для NH:
    http://sourceforge.net/projects/nhibernateaddin или http://altinoren.com/activewriter/

    С другой стороны раз уж MS хотят забыть l2s и перевести всё на EF может во второй версии дизайнер будет действительно хорошим.

    ReplyDelete
  4. maxim: Согласен, что ошибки в дизайнере есть, но я не сказал бы, чтобы так уж много. Релизная версия дизайнера НАМНОГО лучше, чем те, что были доступны в бете :) Да, в бете мне тоже приходилось не раз лазить в XML и править его руцями, но в релизной версии пока что таких серьезных напрягов не было.

    Сейчас я вижу 3 основные проблемы с дизайнером:
    1) Так как edmx-файл включает в себя не только csdl, msl и ssdl, а еще и координаты положения объектов в дизайнере, то сценарий одновременного редактирования модели с последующим мержингом работает плохо. Решение - править модель по очереди :)
    2) В дизайнере показывается лишь концептуальная модель (то есть классы) с маппингом на модель базы данных, в то же время самой этой модели в дизайнере нет. То есть если Update прошел как-то не так, то нет никакой возможности пофиксить проблему в дизайнере. Решение - фиксить XML вручную
    3) Иногда студия глючит и не дает открыть edmx-файл в дизайнере. Это баг. Решение тут довольно простое - откройте edmx как xml и потом, не закрывая его, попробуйте снова открыть edmx как обычно. Студия спрашивает, хотим ли мы закрыть текущий открытый XML и заменить его дизайнером, мы говорим да - и все отлично.

    Спасибо за ссылки на тулы для NHibernate. Посмотрю на досуге :)

    ReplyDelete
  5. Почему на блоге так мало тем про кризис, Вас этот вопрос не волнует?

    ReplyDelete
  6. Вот это вопрос - даже не знаю, что ответить :)

    Во-первых, кризис не входит в сферу моих интересов, поэтому что-то про него писать нет желания.

    Во-вторых, я не экономист и не эксперт в этом вопросе. Лучше читать мысли профессионалов в этой области, чем дилетантов. А я лучше буду писать про то, в чем разбираюсь сам.

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

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

    ReplyDelete
  7. Кстати да, часто!

    ReplyDelete
  8. спасибо за статью… добавил в ридер

    ReplyDelete
  9. A chto dlya vas vash blog? Vi proffesional’niy blogger ili eto prosto dlya dushi?

    ReplyDelete
  10. Отлично!!! Вместо книги на ночь.

    ReplyDelete
  11. 2 one of the Anonymouses:
    Просто для души. Интересно с кем-то делится какими-то находками, результатами работы или даже просто мыслями. А еще приятно, когда то, что я пишу, кому-то помогает в работе.

    2 all the Anonymouses:
    Приятно, что кому-то было полезно! Пишите свои имена или ники, чтобы можно было к кому-то конкретно обращаться, а то получается, как сейчас :)

    ReplyDelete
  12. Классная статья - спасибо!

    ReplyDelete
  13. Поудалял половину анонимных комментариев, так как сильно смахивают на автоматическую рассылку. В последнее время это становится популярным на блоге. Ну, и добавил капчу на будущее...

    Если Ваш комментарий был удален - не судите строго - можете написать его заново :)

    ReplyDelete