У меня есть класс, как это:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
На самом деле это намного больше, но это воссоздает проблему (странность).
Я хочу получить сумму Value
, где экземпляр действителен. Пока что я нашел два решения для этого.
Первый такой:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
Второй, однако, таков:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Я хочу получить наиболее эффективный метод. Сначала я думал, что второй будет более эффективным. Затем теоретическая часть меня начала звучать так: «Один из них - O (n + m + m), другой - O (n + n). Первый должен работать лучше с большим количеством инвалидов, а второй должен работать лучше. менее". Я думал, что они будут работать одинаково. РЕДАКТИРОВАТЬ: И затем @Martin указал, что Where и Select были объединены, так что на самом деле это должно быть O (m + n). Однако, если вы посмотрите ниже, кажется, что это не связано.
Поэтому я проверил это.
(Это более 100 строк, поэтому я подумал, что было бы лучше опубликовать его в виде Gist.)
Результаты были ... интересными.
С 0% допуска на связь:
Весы в пользу Select
и Where
примерно на ~ 30 баллов.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
С 2% терпимости связи:
То же самое, за исключением того, что для некоторых они были в пределах 2%. Я бы сказал, что это минимальный предел погрешности. Select
и Where
теперь у вас всего 20 очков.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
С 5% терпимости связи:
Это то, что я бы сказал, чтобы быть моей максимальной погрешностью. Это делает его немного лучше Select
, но не намного.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
С допуском 10%:
Это выход из моей погрешности, но я все еще заинтересован в результате. Потому что это дает Select
и Where
двадцать точки вести это теперь было на некоторое время.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
С допуском 25%:
Это путь, выход за пределы моей погрешности, но я все еще заинтересован в результате, потому что Select
и Where
до сих пор (почти) сохраняют свое преимущество в 20 очков. Кажется, что он превосходит его в нескольких отдельных, и это то, что дает ему преимущество.
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Теперь, я предполагаю , что 20 очка пришли с середины, где они оба обязаны получить вокруг той же производительности. Я мог бы попытаться записать это, но это была бы целая куча информации, чтобы принять. График был бы лучше, я думаю.
Так вот что я сделал.
Это показывает, что Select
линия остается устойчивой (ожидаемой) и что Select + Where
линия поднимается вверх (ожидаемой). Однако, что озадачивает меня, почему он не встретится с Select
на 50 или выше: на самом деле я ожидал раньше , чем 50, в качестве дополнительного переписчик должен был быть создан для Select
и Where
. Я имею в виду, это показывает преимущество в 20 пунктов, но это не объясняет почему. Это, наверное, главное в моем вопросе.
Почему это ведет себя так? Должен ли я доверять этому? Если нет, я должен использовать другой или этот?
Как упоминалось в комментариях @KingKong, вы также можете использовать Sum
перегрузку, которая принимает лямбду. Таким образом, мои два варианта теперь изменены на это:
Первый:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Во-вторых:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Я собираюсь сделать это немного короче, но:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
Двадцать пунктов свинца все еще там, а это означает , что не нужно делать с Where
и Select
комбинации указывали @Marcin в комментариях.
Спасибо за чтение моей стены текста! Кроме того, если вам интересно, вот измененная версия, которая регистрирует CSV, который принимает Excel.
mc.Value
ней.Where
+Select
не вызывает двух отдельных итераций над коллекцией ввода. LINQ to Objects оптимизирует его в одну итерацию. Читайте Больше на моем сообщении в блогеОтветы:
Select
выполняет итерации один раз по всему набору и для каждого элемента выполняет условную ветвь (проверку на достоверность) и+
операцию.Where+Select
создает итератор, который пропускает недопустимые элементы (неyield
их), выполняя+
только допустимые элементы.Итак, стоимость за это
Select
:И для
Where+Select
:Куда:
p(valid)
вероятность того, что элемент в списке является действительным.cost(check valid)
стоимость ветки, которая проверяет правильностьcost(yield)
это стоимость создания нового состоянияwhere
итератора, которое является более сложным, чем простой итератор, которыйSelect
использует версия.Как вы можете видеть, для заданного
n
, тоSelect
версия является постоянной, в то время какWhere+Select
версия представляет собой линейное уравнение с вp(valid)
качестве переменной. Фактические значения затрат определяют точку пересечения двух линий, и, поскольку ониcost(yield)
могут отличаться от нихcost(+)
, они не обязательно пересекаются приp(valid)
= 0,5.источник
cost(append)
? Действительно хороший ответ, хотя, смотрит на это с другой точки зрения, а не просто статистика.Where
ничего не создает, просто возвращает один элемент за раз изsource
последовательности, если только он заполняет предикат.IEnumerable.Select(IEnumerable, Func)
иIQueryable.Select(IQueryable, Expression<Func>)
. Вы правы в том, что LINQ ничего не делает до тех пор, пока вы не перебираете коллекцию, что, вероятно, вы и имели в виду.Вот подробное объяснение причин различий во времени.
Sum()
ФункцияIEnumerable<int>
выглядит следующим образом :В C #
foreach
это просто синтаксический сахар для .Net версии итератора (не путать с ) . Таким образом, приведенный выше код фактически переведен на это:IEnumerator<T>
IEnumerable<T>
Помните, что две строки кода, которые вы сравниваете:
Теперь вот кикер:
LINQ использует отложенное выполнение . Таким образом, хотя может показаться, что
result1
итерация по коллекции дважды, но фактически она повторяется только один раз.Where()
Условие фактически применяется во времяSum()
, внутри вызоваMoveNext()
(Это возможно благодаря магииyield return
) .Это означает, что для
result1
кода внутриwhile
цикла,выполняется только один раз для каждого элемента с
mc.IsValid == true
. Для сравнения,result2
выполнит этот код для каждого элемента в коллекции. Вот почему,result1
как правило, быстрее.(Тем не менее, обратите внимание, что вызов
Where()
условия внутриMoveNext()
все еще имеет небольшие накладные расходы, поэтому, если большинство / все элементы имеютmc.IsValid == true
, наresult2
самом деле будет быстрее!)Надеюсь, теперь понятно, почему
result2
обычно медленнее. Теперь я хотел бы объяснить, почему я заявил в комментариях, что эти сравнения производительности LINQ не имеют значения .Создание выражения LINQ дешево. Вызов функций делегата обходится дешево. Распределение и циклы по итератору дешевы. Но даже дешевле не делать этих вещей. Таким образом, если вы обнаружите, что оператор LINQ является узким местом в вашей программе, по моему опыту переписывание его без LINQ всегда сделает его быстрее, чем любой из различных методов LINQ.
Итак, ваш рабочий процесс LINQ должен выглядеть следующим образом:
К счастью, узкие места LINQ редки. Черт, узкие места редки. За последние несколько лет я написал сотни утверждений LINQ и в итоге заменил <1%. И большинство из них были связаны с плохой оптимизацией SQL в LINQ2EF , а не по вине LINQ.
Так что , как всегда, напишите четкий и разумный код первого, и ждать , пока после того, как вы профилированные , чтобы беспокоиться о микро-оптимизации.
источник
Забавная вещь. Вы знаете, как это
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
определяется? Он используетSelect
метод!Так что на самом деле все должно работать почти одинаково. Я провел быстрое исследование самостоятельно, и вот результаты:
Для следующих реализаций:
mod
означает: каждый 1 изmod
элементов недействителен: дляmod == 1
каждого элемента недопустим, дляmod == 2
нечетных элементов недействительны и т. д. Коллекция содержит10000000
элементы.И результаты для сбора с
100000000
предметами:Как видите,
Select
иSum
результаты вполне согласуются по всемmod
значениям. Однакоwhere
иwhere
+select
нет.источник
Я предполагаю, что версия с Where отфильтровывает 0, и они не являются предметом для Sum (т.е. вы не выполняете сложение). Это, конечно, предположение, так как я не могу объяснить, как выполнение дополнительного лямбда-выражения и вызов нескольких методов превосходит простое добавление 0.
Мой друг предположил, что тот факт, что 0 в сумме может привести к серьезному снижению производительности из-за проверок переполнения. Было бы интересно посмотреть, как это будет работать в непроверенном контексте.
источник
unchecked
делают его чуть-чуть лучше дляSelect
.Запустив следующий пример, мне становится ясно, что единственный способ, где Where + Select может превзойти Select, - это когда фактически отбрасывается значительное количество (примерно половина в моих неофициальных тестах) потенциальных элементов в списке. В приведенном ниже небольшом примере я получаю примерно одинаковые числа из обоих образцов, когда Where пропускает примерно 4 миллиона пунктов из 10 миллионов. Я побежал в выпуске, и переупорядочил выполнение, где + выберите против выбора с теми же результатами.
источник
Select
?Если вам нужна скорость, лучше всего сделать простой цикл. И дела,
for
как правило, лучше, чемforeach
(при условии, что ваша коллекция, конечно, с произвольным доступом).Вот время, которое я получил с 10% недопустимых элементов:
И с 90% недопустимых элементов:
И вот мой тестовый код ...
Кстати, я согласен с предположением Стилгара : относительные скорости ваших двух дел варьируются в зависимости от процента недопустимых предметов, просто потому, что объем работы, которую
Sum
необходимо выполнить, зависит от случая «Где».источник
Вместо того, чтобы пытаться объяснить через описание, я собираюсь использовать более математический подход.
Учитывая приведенный ниже код, который должен приблизительно соответствовать тому, что делает LINQ внутри компании, относительные затраты следующие:
Только выбор:
Nd + Na
Где + Выбор:
Nd + Md + Ma
Чтобы выяснить точку их пересечения, нам нужно сделать небольшую алгебру:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
Это означает, что для того, чтобы точка перегиба составляла 50%, стоимость вызова делегата должна быть примерно равна стоимости добавления. Поскольку мы знаем, что фактическая точка перегиба составляла примерно 60%, мы можем работать в обратном направлении и определить, что стоимость вызова делегата для @ It'sNotALie фактически составляла примерно 2/3 стоимости добавления, что удивительно, но вот что его номера говорят.
источник
Я думаю, это интересно, что результат MarcinJuraszek отличается от результата It'sNotALie. В частности, результаты MarcinJuraszek начинаются со всех четырех реализаций в одном месте, в то время как результаты It'sNotALie пересекают середину. Я объясню, как это работает из источника.
Предположим, что есть
n
общие элементы иm
допустимые элементы.Sum
Функция довольно проста. Он просто перебирает перечислитель: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Для простоты предположим, что коллекция представляет собой список. И Select, и WhereSelect создадут
WhereSelectListIterator
. Это означает, что фактические сгенерированные итераторы одинаковы. В обоих случаях существуетSum
итератор, который зацикливается на итератореWhereSelectListIterator
. Наиболее интересной частью итератора является метод MoveNext .Поскольку итераторы одинаковы, циклы одинаковы. Разница только в теле петель.
Тело этих лямбд имеет очень сходную стоимость. Предложение where возвращает значение поля, а троичный предикат также возвращает значение поля. Предложение select возвращает значение поля, а две ветви троичного оператора возвращают либо значение поля, либо константу. Комбинированное предложение select имеет ответвление как троичный оператор, но WhereSelect использует ответвление в
MoveNext
.Однако все эти операции довольно дешевы. Самая дорогая операция на сегодняшний день - это филиал, где нам будет стоить неправильный прогноз.
Еще одна дорогая операция здесь
Invoke
. Вызов функции занимает немного больше времени, чем добавление значения, как показал Бранко Димитриевич.Также взвешиванием является проверенное накопление в
Sum
. Если процессор не имеет арифметического флага переполнения, то это также может быть дорогостоящим для проверки.Следовательно, интересные затраты:
n
+m
) * Invoke +m
*checked+=
n
* Вызвать +n
*checked+=
Таким образом, если стоимость Invoke намного выше стоимости проверенного накопления, то случай 2 всегда лучше. Если они примерно равны, то мы увидим баланс, когда около половины элементов являются действительными.
Похоже, что в системе MarcinJuraszek проверенный + = имеет незначительную стоимость, но в системах It'sNotALie и Branko Dimitrijevic, проверенный + = имеет значительные затраты. Похоже, что это самая дорогая в системе It'sNotALie, поскольку точка безубыточности намного выше. Не похоже, что кто-то опубликовал результаты из системы, где накопление стоит намного дороже, чем Invoke.
источник