Сегодня я хотел бы начать серию заметок о сравнении производительности Entity Framework, LINQ to SQL, Active Record (NHibernate) и классического ADO.NET. Я еще не знаю, во что выльется эта идея, и к чему мы придем в результате, но основными целями этого сравнения я бы назвал:
- получение адекватных результатов сравнения и попытка их объяснить
- обсуждения различных вариантов увеличения производительности вышеуказанных способов доступа к базе данных
- получение понимания, какие варианты доступа к базе данных являются наиболее приемлемыми в той или иной ситуации с точки зрения производительности
Сразу скажу, что, несмотря на то, что я уже почти год использую 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 – пожалуйста. Мои тесты покрывают лишь дефолтные настройки.
Полезные ссылки:
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?