Какие проблемы производительности могут возникать при использовании LINQ
Что проверяют
Различаете ли вы LINQ to Objects и LINQ поверх IQueryable, где запрос превращается в SQL. Следом почти всегда дают кусок кода с ToList() посередине цепочки и просят найти, где данные уезжают в память.
Как отвечать
Разделите ответ на две части. В памяти:
- повторный перебор: запрос ленивый, и каждый
foreachпо одномуIEnumerableвыполняет всю цепочку заново. Если результат нужен дважды, материализуйте его один раз; - аллокации: итераторы, делегаты и замыкания, захватившие переменные, создают объекты на каждый вызов. В горячем цикле это нагрузка на GC, и обычный
forтам заметно быстрее. Многие операторы в последних версиях .NET ускорили, но замыкания никуда не делись; - скрытая квадратичность:
Where(x => list.Contains(x))поListэто O(n·m), сHashSetбудет линейно. Сюда жеCount() > 0вместоAny()иOrderBy().First()вместоMinBy(); AsParallel(): на маленьких коллекциях накладные расходы съедают выигрыш.
С EF Core:
- ранняя материализация:
ToList()илиAsEnumerable()доWhereвытаскивает всю таблицу и фильтрует уже в памяти; - лишние колонки и трекинг: без
Selectв DTO тянутся все поля, а для чтения нуженAsNoTracking(); - N+1: ленивая загрузка навигационных свойств в цикле даёт запрос на каждую строку. Решается
Includeили проекцией, а при несколькихIncludeколлекций стоит посмотреть наAsSplitQuery()из-за декартова взрыва.
Закончите тем, как вы проверяете: ToQueryString() и логирование SQL для EF, BenchmarkDotNet с MemoryDiagnoser для кода в памяти.
Где спотыкаются
Советуют «вызвать ToList(), чтобы было быстрее», не понимая, что для IQueryable это загрузка всей выборки в память.
Путают IEnumerable и IQueryable в сигнатуре репозитория: метод возвращает IEnumerable, и дальнейший Where уже не попадает в SQL.
Говорят о производительности LINQ без замеров. Интервьюер ждёт, что вы назовёте инструмент и покажете реальное узкое место, а не перечислите мифы.