Асинхронная операция Entity Framework занимает в десять раз больше времени

У меня есть сайт MVC, который использует Entity Framework 6 для обработки базы данных, и я экспериментировал с его изменением, так что все работает как async-controllerы и вызовы в базу данных запускаются как их асинхронные копии (например, ToListAsync () вместо ToList ())

Проблема, с которой я столкнулась, заключается в том, что простое изменение моих запросов на асинхронность заставил их быть невероятно медленными.

Следующий код получает коллекцию объектов «Альбом» из моего контекста данных и переводится на довольно простое соединение с базой данных:

// Get the albums var albums = await this.context.Albums .Where(x => x.Artist.ID == artist.ID) .ToListAsync(); 

Вот созданный SQL:

 exec sp_executesql N'SELECT [Extent1].[ID] AS [ID], [Extent1].[URL] AS [URL], [Extent1].[ASIN] AS [ASIN], [Extent1].[Title] AS [Title], [Extent1].[ReleaseDate] AS [ReleaseDate], [Extent1].[AccurateDay] AS [AccurateDay], [Extent1].[AccurateMonth] AS [AccurateMonth], [Extent1].[Type] AS [Type], [Extent1].[Tracks] AS [Tracks], [Extent1].[MainCredits] AS [MainCredits], [Extent1].[SupportingCredits] AS [SupportingCredits], [Extent1].[Description] AS [Description], [Extent1].[Image] AS [Image], [Extent1].[HasImage] AS [HasImage], [Extent1].[Created] AS [Created], [Extent1].[Artist_ID] AS [Artist_ID] FROM [dbo].[Albums] AS [Extent1] WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134 

По сути дела, это не очень сложный запрос, но для запуска SQL Server занимает почти 6 секунд. Провайдер SQL Server сообщает об этом как 5742 мс для завершения.

Если я изменю свой код на:

 // Get the albums var albums = this.context.Albums .Where(x => x.Artist.ID == artist.ID) .ToList(); 

Затем генерируется тот же самый SQL, но он работает только в 474 мс в соответствии с SQL Server Profiler.

База данных содержит около 3500 строк в таблице «Альбомы», чего не очень много, и имеет индекс в столбце «Artist_ID», поэтому это должно быть довольно быстро.

Я знаю, что у aync есть накладные расходы, но делать вещи в десять раз медленнее, кажется немного крутым для меня! Где я здесь не так?

Я нашел этот вопрос очень интересным, тем более, что я использую async везде с Ado.Net и EF 6. Я надеялся, что кто-то объяснит этот вопрос, но этого не произошло. Поэтому я попытался воспроизвести эту проблему на моей стороне. Надеюсь, некоторые из вас найдут это интересным.

Первые хорошие новости: я воспроизвел это 🙂 И разница огромна. С коэффициентом 8 …

первые результаты

Сначала я подозревал что-то, что CommandBehavior , так как я прочитал интересную статью об async Ado, сказав следующее:

«Поскольку режим не последовательного доступа должен хранить данные для всей строки, это может вызвать проблемы, если вы читаете большой столбец с сервера (например, varbinary (MAX), varchar (MAX), nvarchar (MAX) или XML ) «.

Я подозревал, что ToList() вызывает CommandBehavior.SequentialAccess и async, которые должны быть CommandBehavior.Default (не последовательные, что может вызвать проблемы). Поэтому я загрузил источники EF6 и поставил точки останова везде (где, конечно, CommandBehavior ).

Результат: ничего . Все вызовы сделаны с CommandBehavior.Default …. Поэтому я попытался войти в EF-код, чтобы понять, что происходит … и .. ooouch … Я никогда не вижу такой делегирующий код, все кажется ленивым исполненным …

Поэтому я попытался сделать некоторые профилирования, чтобы понять, что происходит …

И я думаю, что у меня есть что-то …

Вот модель для создания таблицы, в которой я сравнивал, с 3500 строк внутри нее и 256 Кб случайных данных в каждой varbinary(MAX) . (EF 6.1 – CodeFirst – CodePlex ):

 public class TestContext : DbContext { public TestContext() : base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance { } public DbSet Items { get; set; } } public class TestItem { public int ID { get; set; } public string Name { get; set; } public byte[] BinaryData { get; set; } } 

И вот код, который я использовал для создания тестовых данных, и бенчмарк EF.

 using (TestContext db = new TestContext()) { if (!db.Items.Any()) { foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines { byte[] dummyData = new byte[1 << 18]; // with 256 Kbyte new Random().NextBytes(dummyData); db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData }); } await db.SaveChangesAsync(); } } using (TestContext db = new TestContext()) // EF Warm Up { var warmItUp = db.Items.FirstOrDefault(); warmItUp = await db.Items.FirstOrDefaultAsync(); } Stopwatch watch = new Stopwatch(); using (TestContext db = new TestContext()) { watch.Start(); var testRegular = db.Items.ToList(); watch.Stop(); Console.WriteLine("non async : " + watch.ElapsedMilliseconds); } using (TestContext db = new TestContext()) { watch.Restart(); var testAsync = await db.Items.ToListAsync(); watch.Stop(); Console.WriteLine("async : " + watch.ElapsedMilliseconds); } using (var connection = new SqlConnection(CS)) { await connection.OpenAsync(); using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection)) { watch.Restart(); List itemsWithAdo = new List(); var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess); while (await reader.ReadAsync()) { var item = new TestItem(); item.ID = (int)reader[0]; item.Name = (String)reader[1]; item.BinaryData = (byte[])reader[2]; itemsWithAdo.Add(item); } watch.Stop(); Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds); } } using (var connection = new SqlConnection(CS)) { await connection.OpenAsync(); using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection)) { watch.Restart(); List itemsWithAdo = new List(); var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default); while (await reader.ReadAsync()) { var item = new TestItem(); item.ID = (int)reader[0]; item.Name = (String)reader[1]; item.BinaryData = (byte[])reader[2]; itemsWithAdo.Add(item); } watch.Stop(); Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds); } } using (var connection = new SqlConnection(CS)) { await connection.OpenAsync(); using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection)) { watch.Restart(); List itemsWithAdo = new List(); var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess); while (reader.Read()) { var item = new TestItem(); item.ID = (int)reader[0]; item.Name = (String)reader[1]; item.BinaryData = (byte[])reader[2]; itemsWithAdo.Add(item); } watch.Stop(); Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds); } } using (var connection = new SqlConnection(CS)) { await connection.OpenAsync(); using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection)) { watch.Restart(); List itemsWithAdo = new List(); var reader = cmd.ExecuteReader(CommandBehavior.Default); while (reader.Read()) { var item = new TestItem(); item.ID = (int)reader[0]; item.Name = (String)reader[1]; item.BinaryData = (byte[])reader[2]; itemsWithAdo.Add(item); } watch.Stop(); Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds); } } 

Для обычного вызова EF ( .ToList() ) профилирование кажется «нормальным» и легко читается:

След ToList

Здесь мы находим 8,4 секунды у нас с секундомером (профилирование замедляет функционал). Мы также находим HitCount = 3500 по пути вызова, что согласуется с 3500 строк в тесте. На стороне анализатора TDS все начинает ухудшаться, поскольку мы читаем 118 353 вызовов TryReadByteArray() , который представляет собой цикл буферизации. (в среднем 33,8 вызова для каждого byte[] из 256 КБ)

Для async случая это действительно действительно отличается. Во-первых, на .ToListAsync() запланирован вызов .ToListAsync() , а затем ожидаемый. Ничего удивительного здесь. Но теперь, вот async ад на ThreadPool:

ToListAsync hell

Во-первых, в первом случае у нас было всего 3500 очков хитов по полному пути вызова, здесь у нас есть 118 371. Кроме того, вы должны представить все вызовы синхронизации, которые я не ставил на скриншоты …

Во-вторых, в первом случае у нас было «всего 118 353» вызовов метода TryReadByteArray() , здесь у нас есть 2 050 210 звонков! Это в 17 раз больше … (при тестировании с большим массивом 1 Мб, это в 160 раз больше)

Кроме того, есть:

  • 120 000 Созданные экземпляры Task
  • 727 519 Interlocked вызовы
  • 290 569 Monitor вызовов
  • 98 283 ExecutionContext экземпляры, с 264 481 Captures
  • 208 733 звонки SpinLock

Я предполагаю, что буферизация выполняется асинхронным способом (а не хорошим), с параллельными задачами, пытающимися считывать данные из TDS. Слишком много задач создаются только для анализа двоичных данных.

Как предварительный вывод, мы можем сказать, что Async велик, EF6 велик, но использование EIN6 в async в его текущей реализации добавляет большие накладные расходы, на стороне производительности, стороне Threading и стороне процессора (12% использования ЦП в ToList() и 20% в случае ToListAsync для работы в 8-10 раз больше … Я запускаю его на старом i7 920).

Пока делал несколько тестов, я снова думал об этой статье, и я замечаю, что я скучаю:

«Для новых асинхронных методов в .Net 4.5 их поведение точно такое же, как и с синхронными методами, за исключением одного заметного исключения: ReadAsync в несекретном режиме».

Какие ?!!!

Поэтому я расширяю свои тесты, чтобы включить Ado.Net в обычный / asynchronous вызов, а также с CommandBehavior.SequentialAccess / CommandBehavior.Default , и вот большой сюрприз! :

с ado

У нас такое же поведение с Ado.Net !!! Facepalm …

Мое окончательное заключение : есть ошибка в реализации EF 6. Он должен переключать CommandBehavior на SequentialAccess когда asynchronous вызов выполняется по таблице, содержащей binary(max) столбец. Проблема создания слишком большого количества задач, замедляющих процесс, находится на стороне Ado.Net. Проблема EF заключается в том, что он не использует Ado.Net, как должен.

Теперь вы знаете вместо использования асинхронных методов EF6, вам лучше будет вызывать EF обычным неасинхронным способом, а затем использовать TaskCompletionSource чтобы вернуть результат асинхронным способом.

Примечание 1: я отредактировал свой пост из-за позорной ошибки …. Я сделал свой первый тест по сети, а не локально, и ограниченная полоса пропускания исказила результаты. Вот обновленные результаты.

Примечание 2: Я не расширил свой тест на другие случаи использования (например: nvarchar(max) с большим количеством данных), но есть вероятность, что такое же поведение происходит.

Примечание 3: Что-то обычное для ToList() – это 12% -ный процессор (1/8 моего CPU = 1 логического ядра). Что-то необычное – это максимум 20% для ToListAsync() , как если бы Планировщик не мог использовать все Протесты. Вероятно, из-за слишком большого количества созданной задачи или, может быть, узкого места в парсере TDS, я не знаю …

  • EF Core Второй уровень ThenInclude missworks
  • Entity Framework - получить идентификатор до «SaveChanges» внутри транзакции
  • Поиск причины DBUpdateException
  • Сообщение об ошибке «Невозможно загрузить один или несколько запрошенных типов. Получите свойство LoaderExceptions для получения дополнительной информации. '
  • Строка соединения с именем «MyEntities» не найдена в файле конфигурации приложения
  • Entity Framework: уже существует открытый DataReader, связанный с этой командой
  • От одного до одного необязательного отношения с использованием API-интерфейса Entity Framework Fluent
  • Почему LINQ to Entities не распознает метод «System.String ToString ()?
  • Entity Framework 6 Первые пользовательские функции кода
  • Шаблон хранилища, POCO и бизнес-объекты
  • Получение всех изменений, внесенных в объект в Entity Framework
  • Давайте будем гением компьютера.