web-dev-qa-db-ja.com

Find()とFirstOrDefault()のパフォーマンス

同様の質問:
Find()vs. Where()。FirstOrDefault()

単一の文字列プロパティを持つ単純な参照型の大きなシーケンス内でダイアナを検索する興味深い結果が得られました。

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()は、を期待して、ほぼ2倍の速度で実行されます。Netチームは、将来的に廃止とマークしません。

96

結果をまねることができたので、プログラムを逆コンパイルしましたが、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);
}

リストは配列のラッパーなので、アイテムの配列を反復処理するのは理にかなっています。

ただし、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は、列挙可能なパターンを使用して syntatic sugar だけです。この画像を見て

enter image description here

Foreachをクリックして、それが何をしているのかを確認しました。dotpeekが列挙子/現在/次の実装に私を連れて行きたいと思っていることがわかります。

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

97
devshorts

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

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

最初の速度とほぼ同じ速度で実行されます(FirstOrDefaultの場合は25ms vs 27ms)

編集:配列ループを追加すると、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反復のオーバーヘッドを処理するよりも高速です。 (しかし、両方とも長所/短所があるため、論理的にコードにとって意味のあるものを選択してください。さらに、速度のわずかな違いeverが生じることはほとんどありません問題なので、保守性/可読性に意味のあるものを使用してください)

24
Chris Sinclair