単一の文字列プロパティを持つ単純な参照型の大きなシーケンス内でダイアナを検索する興味深い結果が得られました。
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()
は、を期待して、ほぼ2倍の速度で実行されます。Netチームは、将来的に廃止とマークしません。
結果をまねることができたので、プログラムを逆コンパイルしましたが、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は何をしているのでしょうか?これは逆コンパイルされた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 だけです。この画像を見て
。
Foreachをクリックして、それが何をしているのかを確認しました。dotpeekが列挙子/現在/次の実装に私を連れて行きたいと思っていることがわかります。
それ以外は基本的に同じです(渡された述語をテストして、アイテムが欲しいものかどうかを確認します)
FirstOrDefault
がIEnumerable
実装を介して実行されていることを確認しています。つまり、標準の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が生じることはほとんどありません問題なので、保守性/可読性に意味のあるものを使用してください)