Производительность поиска() vs. FirstOrDefault()
Возможный дубликат:
Найти() vs. Where(). FirstOrDefault()
Получил интересный результат для поиска Дианы в большой последовательности простого ссылочного типа, имеющего однострочное свойство.
Stopwatch watch = new Stopwatch();
string diana = "Diana";
while (Console.ReadKey().Key != ConsoleKey.Escape)
{
//Armour with 1000k++ customers. Wow, should be a product with a great success! :)
var customers = (from i in Enumerable.Range(0, 1000000)
select new Customer
{
Name = Guid.NewGuid().ToString()
}).ToList();
customers.Insert(999000, new Customer { Name = diana }); // Putting Diana at the end :)
//1. System.Linq.Enumerable.DefaultOrFirst()
watch.Restart();
customers.FirstOrDefault(c => c.Name == diana);
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Linq.Enumerable.FirstOrDefault().", watch.ElapsedMilliseconds);
//2. System.Collections.Generic.List<T>.Find()
watch.Restart();
customers.Find(c => c.Name == diana);
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T>.Find().", watch.ElapsedMilliseconds);
}
![enter image description here]()
Это из-за отсутствия служебных ресурсов Enumerator в List.Find() или этого плюс, возможно, что-то еще?
Find()
работает почти в два раза быстрее, надеясь, что команда .Net не будет отмечать его Устаревшее в будущем.
Ответы
Ответ 1
Мне удалось подражать вашим результатам, поэтому я декомпилировал вашу программу, и есть разница между Find
и FirstOrDefault
.
Прежде всего, это декомпилированная программа. Я сделал ваш объект данных анонимным элементом данных только для компиляции
List<\u003C\u003Ef__AnonymousType0<string>> source = Enumerable.ToList(Enumerable.Select(Enumerable.Range(0, 1000000), i =>
{
var local_0 = new
{
Name = Guid.NewGuid().ToString()
};
return local_0;
}));
source.Insert(999000, new
{
Name = diana
});
stopwatch.Restart();
Enumerable.FirstOrDefault(source, c => c.Name == diana);
stopwatch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Linq.Enumerable.FirstOrDefault().", (object) stopwatch.ElapsedMilliseconds);
stopwatch.Restart();
source.Find(c => c.Name == diana);
stopwatch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T>.Find().", (object) stopwatch.ElapsedMilliseconds);
Ключевое замечание здесь состоит в том, что FirstOrDefault
вызывается в Enumerable
, тогда как Find
вызывается как метод в списке источников.
Итак, что находят? Это декомпилированный Find
метод
private T[] _items;
[__DynamicallyInvokable]
public T Find(Predicate<T> match)
{
if (match == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
for (int index = 0; index < this._size; ++index)
{
if (match(this._items[index]))
return this._items[index];
}
return default (T);
}
Таким образом, он выполняет итерацию по массиву элементов, что имеет смысл, поскольку список является оболочкой массива.
Однако FirstOrDefault
в классе Enumerable
использует foreach
для повторения элементов. Это использует итератор в списке и перемещается дальше. Я думаю, что то, что вы видите, это накладные расходы итератора
[__DynamicallyInvokable]
public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
throw Error.ArgumentNull("source");
if (predicate == null)
throw Error.ArgumentNull("predicate");
foreach (TSource source1 in source)
{
if (predicate(source1))
return source1;
}
return default (TSource);
}
Foreach - это синтаксический сахар по использованию перечислимого шаблона. Посмотрите на это изображение
.
Я нажал на foreach, чтобы увидеть, что он делает, и вы можете видеть, что dotpeek хочет взять меня в enumerator/current/next реализаций, что имеет смысл.
Кроме того, они в основном одинаковы (тестирование переданного в предикате, чтобы увидеть, нужен ли элемент)
Ответ 2
Я ставлю, что FirstOrDefault
работает через реализацию IEnumerable
, то есть будет использовать стандартный цикл foreach
для проверки. List<T>.Find()
не является частью Linq (http://msdn.microsoft.com/en-us/library/x0b5b5bc.aspx) и, вероятно, использует стандартный цикл for
от 0
до Count
(или другой быстрый внутренний механизм, вероятно, работает непосредственно на его внутреннем/завернутом массиве). Избавляясь от накладных расходов путем перечисления (и выполняя проверки версии, чтобы гарантировать, что список не был изменен), метод Find
выполняется быстрее.
Если вы добавите третий тест:
//3. System.Collections.Generic.List<T> foreach
Func<Customer, bool> dianaCheck = c => c.Name == diana;
watch.Restart();
foreach(var c in customers)
{
if (dianaCheck(c))
break;
}
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T> foreach.", watch.ElapsedMilliseconds);
Это работает с той же скоростью, что и первая (25 мс против 27 мс для FirstOrDefault
)
EDIT: если я добавлю цикл массива, он будет близок к скорости Find()
, и если @devshorts заглянет в исходный код, я думаю, что это:
//4. System.Collections.Generic.List<T> for loop
var customersArray = customers.ToArray();
watch.Restart();
int customersCount = customersArray.Length;
for (int i = 0; i < customersCount; i++)
{
if (dianaCheck(customers[i]))
break;
}
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with an array for loop.", watch.ElapsedMilliseconds);
Это работает на 5.5% медленнее, чем метод Find()
.
Итак, нижняя строка: цикл по элементам массива быстрее, чем выполнение foreach
накладных расходов итерации. (но оба имеют свои плюсы и минусы, поэтому просто выбирайте, что имеет смысл для вашего логического кода). Кроме того, только редко небольшая разница в скорости когда-либо вызывает проблему, поэтому просто используйте то, что имеет смысл для удобства и удобочитаемости).