关于c#:Find()与FirstOrDefault()的性能

Performance of Find() vs. FirstOrDefault()

本问题已经有最佳答案,请猛点这里访问。

Similar Question:
Find() vs. Where().FirstOrDefault()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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()的运行速度几乎是它的两倍,希望.NET团队将来不会将其标记为过时。


我能模仿你的结果,所以我对你的程序进行了反编译,在FindFirstOrDefault之间有区别。

首先是反编译程序。我把你的数据对象变成了一个你的数据项,只是为了编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

所以它迭代一个有意义的项数组,因为列表是数组上的包装器。

但是,在Enumerable类上,FirstOrDefault使用foreach迭代这些项。这将使用一个迭代器来访问列表并移到下一个。我认为您看到的是迭代器的开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[__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查看它在做什么,你可以看到dotpeek想带我去枚举器/当前/下一个有意义的实现。

除此之外,它们基本上是相同的(测试传入的谓词以查看项是否是您想要的)。


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

如果添加第三个测试:

1
2
3
4
5
6
7
8
9
10
//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)。

编辑:如果我添加一个数组循环,它将非常接近于Find()的速度,并且给定@devshort查看源代码,我认为这是:

1
2
3
4
5
6
7
8
9
10
11
//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迭代开销要快。(但两者都有其优缺点,所以只需选择逻辑上对代码有意义的内容。此外,速度上的微小差异很少会引起问题,因此只需使用对可维护性/可读性有意义的内容)