Разберите производительность и потенциальные проблемы этого C# LINQ-запроса: var res = items.Select(i=>Expensive(i)).Where(x=>x.Flag).ToList(); Где возможны лишние вычисления и как переписать для ленивой оценки?
Коротко — в текущем виде возможны лишние (дорогие) вычисления: поскольку у вас сначала `Select(Expensive)`, а в конце `ToList()`, то `Expensive` будет вызвана для каждого элемента последовательности при полной материализации. Численно — если в исходном `items` ровно NNN элементов, то будет выполнено NNN вызовов `Expensive`. Если только MMM элементов имеют `Flag == true` и M≪NM \ll NM≪N, вы всё равно платите за все NNN вызовов. Проблемы и подводные камни - Ненужные вычисления: если `Flag` определяется только результатом `Expensive`, вы не можете избежать вызова `Expensive` для каждого проверяемого элемента, если фильтр ставится после проекции. - Двойные вычисления: запись вроде `items.Where(i => Expensive(i).Flag).Select(i => Expensive(i)).ToList()` вызовет `Expensive` дважды для прошедших элементов — ошибка. - Память: `ToList()` материализует весь результат; при больших коллекциях может быть расход памяти. - Побочные эффекты и исключения: переупорядочивание операций или ленивость меняют момент и частоту вызовов `Expensive`. - Параллельность: если `Expensive` CPU-bound, можно рассмотреть PLINQ, но нужно учитывать потокобезопасность. Как переписать для ленивой оценки и уменьшения лишних вычислений 1) Если можно отфильтровать по свойствам исходного элемента (дешёвая проверка) — делайте `Where` раньше: var res = items.Where(i => CheapPredicate(i)).Select(i => Expensive(i)).ToList(); тогда `Expensive` вызовется только для MMM элементов, где M≤NM \le NM≤N. 2) Если нужно ленивое перечисление (не материализовать сразу): var res = items.Select(i => Expensive(i)).Where(x => x.Flag); `res` — `IEnumerable`; вычисление произойдёт по мере перечисления (и по-прежнему вызов `Expensive` для каждого перечисленного элемента). 3) Если нужно фильтровать по результату `Expensive`, но избежать лишних аллокаций и дублирования — явный итератор (лениво, без анонимных объектов): IEnumerable Filtered() { foreach (var i in items) { var r = Expensive(i); if (r.Flag) yield return r; } } var res = Filtered(); // лениво, вызовы Expensive по мере перечисления 4) Если вы хотите остановиться после первых k совпадений (короткое замыкание): var res = items.Select(i => Expensive(i)).Where(x => x.Flag).Take(k).ToList(); здесь благодаря ленивости LINQ `Expensive` вызовется не более, чем пока найдено kkk подходящих элементов. 5) Избегайте двух вызовов `Expensive` — если нужно одновременно фильтр и проекция от результата, кэшируйте результат один раз: var res = items.Select(i => { var r = Expensive(i); return new { r }; }) .Where(x => x.r.Flag) .Select(x => x.r) .ToList(); (это всё ещё выполнит `Expensive` для всех элементов; полезно только чтобы не вызывать дважды). Дополнительно: если `Expensive` CPU-bound и независима между элементами, подумайте об `AsParallel()`/PLINQ (требует потокобезопасности). Вывод: если фильтр зависит только от результата `Expensive`, избежать вызова `Expensive` для "отфильтрованных" элементов нельзя — нужно либо фильтровать заранее по дешёвому условию, либо делать ленивое перечисление/раннее завершение (Take) или распараллеливание при необходимости.
Проблемы и подводные камни
- Ненужные вычисления: если `Flag` определяется только результатом `Expensive`, вы не можете избежать вызова `Expensive` для каждого проверяемого элемента, если фильтр ставится после проекции.
- Двойные вычисления: запись вроде `items.Where(i => Expensive(i).Flag).Select(i => Expensive(i)).ToList()` вызовет `Expensive` дважды для прошедших элементов — ошибка.
- Память: `ToList()` материализует весь результат; при больших коллекциях может быть расход памяти.
- Побочные эффекты и исключения: переупорядочивание операций или ленивость меняют момент и частоту вызовов `Expensive`.
- Параллельность: если `Expensive` CPU-bound, можно рассмотреть PLINQ, но нужно учитывать потокобезопасность.
Как переписать для ленивой оценки и уменьшения лишних вычислений
1) Если можно отфильтровать по свойствам исходного элемента (дешёвая проверка) — делайте `Where` раньше:
var res = items.Where(i => CheapPredicate(i)).Select(i => Expensive(i)).ToList();
тогда `Expensive` вызовется только для MMM элементов, где M≤NM \le NM≤N.
2) Если нужно ленивое перечисление (не материализовать сразу):
var res = items.Select(i => Expensive(i)).Where(x => x.Flag);
`res` — `IEnumerable`; вычисление произойдёт по мере перечисления (и по-прежнему вызов `Expensive` для каждого перечисленного элемента).
3) Если нужно фильтровать по результату `Expensive`, но избежать лишних аллокаций и дублирования — явный итератор (лениво, без анонимных объектов):
IEnumerable Filtered()
{
foreach (var i in items)
{
var r = Expensive(i);
if (r.Flag) yield return r;
}
}
var res = Filtered(); // лениво, вызовы Expensive по мере перечисления
4) Если вы хотите остановиться после первых k совпадений (короткое замыкание):
var res = items.Select(i => Expensive(i)).Where(x => x.Flag).Take(k).ToList();
здесь благодаря ленивости LINQ `Expensive` вызовется не более, чем пока найдено kkk подходящих элементов.
5) Избегайте двух вызовов `Expensive` — если нужно одновременно фильтр и проекция от результата, кэшируйте результат один раз:
var res = items.Select(i => { var r = Expensive(i); return new { r }; })
.Where(x => x.r.Flag)
.Select(x => x.r)
.ToList();
(это всё ещё выполнит `Expensive` для всех элементов; полезно только чтобы не вызывать дважды).
Дополнительно: если `Expensive` CPU-bound и независима между элементами, подумайте об `AsParallel()`/PLINQ (требует потокобезопасности).
Вывод: если фильтр зависит только от результата `Expensive`, избежать вызова `Expensive` для "отфильтрованных" элементов нельзя — нужно либо фильтровать заранее по дешёвому условию, либо делать ленивое перечисление/раннее завершение (Take) или распараллеливание при необходимости.