Find()与 FirstOrDefault()的性能

类似的问题:
Find () vs. Where () . FirstOrDefault ()

在一个具有单个字符串属性的简单引用类型的大序列中搜索 Diana 得到了一个有趣的结果。

using System;
using System.Collections.Generic;
using System.Linq;


public class Customer{
public string Name {get;set;}
}


Stopwatch watch = new Stopwatch();
const 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

是因为 List 中没有枚举器开销。 Find ()还是这个加上了其他什么?

Find()运行几乎两倍的速度,希望 。网团队将来不会标记它过时。

101440 次浏览

我能够模拟你的结果,所以我反编译你的程序,有一个区别之间的 FindFirstOrDefault

首先是反编译程序。我将数据对象设置为一个匿名数据项,仅用于编译

    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);

这里需要注意的关键是,FirstOrDefaultEnumerable上调用,而 Find在源列表中作为一个方法调用。

那么,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);
}

因此它迭代一个条目数组,这是有意义的,因为列表是一个数组的包装器。

但是,FirstOrDefaultEnumerable类上使用 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 只是使用可枚举模式的 合成糖

enter image description here.

我点击 foreach 来查看它在做什么,你可以看到 dotpeks 想要带我去枚举器/当前/下一个实现,这是有意义的。

除此之外,它们基本上是相同的(测试谓词中传递的内容,看看某个项是否是您想要的)

我打赌 FirstOrDefault是通过 IEnumerable实现运行的,也就是说,它将使用一个标准的 foreach循环来进行检查。List<T>.Find()不是 Linq (http://msdn.microsoft.com/en-us/library/x0b5b5bc.aspx)的一部分,可能使用从 0Count的标准 for循环(或者另一种可能直接在其内部/包装阵列上运行的快速内部机制)。通过消除枚举的开销(并进行版本检查以确保列表没有被修改) ,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);

它的运行速度与第一个基本相同(FirstOrDefault为25毫秒,FirstOrDefault为27毫秒)

编辑: 如果我添加一个数组循环,它会非常接近 Find()的速度,并且给@devshort 一瞥源代码,我想这就是:

//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);

这比 Find()方法只慢5.5% 。

所以底线是: 通过数组元素进行循环比处理 foreach迭代开销要快。(但是两者都有各自的优缺点,所以只要选择对您的代码有逻辑意义的代码就可以了。此外,永远不会的速度差异很少会导致问题,所以只要使用对可维护性/可读性有意义的内容就可以了)