130

同様の質問:
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);
    }

ここに画像の説明を入力

これは、List.Find() に列挙子のオーバーヘッドがないためですか、それともこれに加えて何か他のことが原因ですか?

Find().Netチームが将来廃止とマークしないことを期待して、ほぼ 2 倍の速度で実行されます。

4

2 に答える 2

130

私はあなたの結果を模倣することができたので、私はあなたのプログラムを逆コンパイル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);

ここで注目すべき重要なことは、 がソース リストのメソッドとして呼び出されるのに対して、 が呼び出されることFirstOrDefaultですEnumerableFind

では、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 が意味のある列挙子/現在/次の実装に私を連れて行こうとしていることがわかります。

それ以外は基本的に同じです(渡された述語をテストして、アイテムが必要なものかどうかを確認します)

于 2012-12-25T17:56:33.807 に答える
27

FirstOrDefault実装を介して実行されていることに賭けていIEnumerableます。つまり、標準foreachループを使用してチェックを行います。List<T>.Find()Linq ( http://msdn.microsoft.com/en-us/library/x0b5b5bc.aspx ) の一部ではなく、 toからの標準forループを使用している可能性があります(または、おそらく内部/ラップされた上で直接動作する別の高速な内部メカニズム配列)。列挙のオーバーヘッドをなくす (そしてリストが変更されていないことを確認するためのバージョン チェックを行う) ことで、このメソッドはより高速になります。0CountFind

3 番目のテストを追加する場合:

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

これは、最初のものとほぼ同じ速度で実行されます (25ms 対 27ms FirstOrDefault) 。

編集:配列ループを追加すると、速度にかなり近づき、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);

Find()これは、メソッドよりもわずか 5.5% 遅く実行されます。

つまり、配列要素をループする方が、foreach反復オーバーヘッドを処理するよりも高速です。(ただし、どちらにも長所/短所があるため、論理的にコードに適したものを選択してください。さらに、速度のわずかな違いが問題を引き起こすことはめったにないため、保守性/読みやすさのために意味のあるものを使用してください)

于 2012-12-25T17:57:01.063 に答える