特定の日の日付と日付の範囲のデータフェッチを実行する2つのクラスがあります。
public class IterationLookup<TItem>
{
private IList<Item> items = null;
public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
{
this.items = items.OrderByDescending(keySelector).ToList();
}
public TItem GetItem(DateTime day)
{
foreach(TItem i in this.items)
{
if (i.IsWithinRange(day))
{
return i;
}
}
return null;
}
}
public class LinqLookup<TItem>
{
private IList<Item> items = null;
public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
{
this.items = items.OrderByDescending(keySelector).ToList();
}
public TItem GetItem(DateTime day)
{
return this.items.FirstOrDefault(i => i.IsWithinRange(day));
}
}
次に、Linqバージョンが約5倍遅いことを示す速度テストを実行します。これは、ToList
を使用してアイテムを列挙せずにローカルにアイテムを保存する場合に意味があります。 FirstOrDefault
を呼び出すたびに、linqもOrderByDescending
を実行するため、これにより処理速度が大幅に低下します。しかし、そうではないので、何が起こっているのか本当にわかりません。 Linqは反復と非常によく似たパフォーマンスを発揮するはずです。
IList<RangeItem> ranges = GenerateRanges(); // returns List<T>
var iterLookup = new IterationLookup<RangeItems>(ranges, r => r.Id);
var linqLookup = new LinqLookup<RangeItems>(ranges, r => r.Id);
Stopwatch timer = new Stopwatch();
timer.Start();
for(int i = 0; i < 1000000; i++)
{
iterLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
linqLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
なぜ私はそれがより良く機能するはずだと知っているのですか?これらのルックアップクラスを使用せずに非常に類似したコードを作成すると、Linqはforeach
の反復と非常に類似したパフォーマンスを実行するためです。
// continue from previous code block
// items used by both order as they do in classes as well
IList<RangeItem> items = ranges.OrderByDescending(r => r.Id).ToList();
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
DateTime day = GetRandomDay();
foreach(RangeItem r in items)
{
if (r.IsWithinRange(day))
{
// RangeItem result = r;
break;
}
}
}
timer.Stop();
// display elapsed time
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
DateTime day = GetRandomDay();
items.FirstOrDefault(i => i.IsWithinRange(day));
}
timer.Stop();
// display elapsed time
これは私の意見では非常によく似たコードです。 FirstOrDefault
私が知っている限り、有効なアイテムに到達するまで、または最後に到達するまで、繰り返します。そして、これはどういうわけかforeach
とbreak
と同じです。
しかし、反復クラスでさえ、私の単純なforeach
反復ループよりもパフォーマンスが劣ります。これは、直接アクセスと比較して、クラス内のメソッドの呼び出しだけであるため、謎でもあります。
LINQクラスのパフォーマンスが非常に遅いので、何が間違っていますか?
Iterationクラスで何が間違っているので、直接のforeach
ループの2倍の速度で実行されますか?
私はこれらのステップを実行します:
ご覧のとおり、オブジェクトのインスタンス化は測定されません。
これらの結果に表示される範囲は重複していません。これにより、LINQバージョンが正常な一致でループを中断しない場合(可能性が高い)、両方のアプローチがさらに類似するはずです。
生成された範囲: ID範囲000000000111111111122222222223300000000011111111112222222222 123456789012345678901234567890112345678901234567890123456789 0922.01.-30.01。 | ---- 0814.01.-16.01。 |-| 0716.02.-19.02。 |-| 0615.01.-17.01。 |-| 0519.02.-23.02。 | --- | 0401.01.-07.01。| ----- | 0302.01.-10.01。 | ------- | 0211.01.-13.01。 |-| 0116.01.-20.01。 | --- | 0029.01.-06.02。 | ------- | ルックアップクラス... -反復:1028ms --Linq:4517ms !!!これIS問題!!! -BitCounter:401ms 手動ループ... -Iter:786ms -Linq:981ms -Iter:787ms -Linq:996ms -Iter:787ms -Linq:977ms -Iter:783ms -Linq: 979ms
完全なコードを自分で入手して何が起こっているかを確認できるように、要点を記載しました。 Consoleアプリケーションを作成し、Program.csをコピーして、この一部である他のファイルを追加します要旨。
それをつかむ---(ここ 。
最も問題だったのはもちろん、非常に遅いLINQimplementatinoでした。これは、デリゲートコンパイラの最適化にすべて関係していることがわかりました。 LukeHは最良で最も使いやすいソリューションを提供しました これにより、実際にさまざまなアプローチを試してみました。 GetItem
メソッド(またはGistで呼び出されるGetPointData
)でさまざまなアプローチを試しました。
ほとんどの開発者が行う通常の方法(Gistにも実装されており、結果が明らかになった後も更新されませんでした):これは最善の方法ではありません。
return this.items.FirstOrDefault(item => item.IsWithinRange(day));
ローカル述語変数を定義することによって:
Func<TItem, bool> predicate = item => item.IsWithinRange(day);
return this.items.FirstOrDefault(predicate);
ローカル述語ビルダー:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d);
return this.items.FirstOrDefault(builder(day));
ローカル述語ビルダーとローカル述語変数:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d);
Func<TItem, bool> predicate = builder(day);
return this.items.FirstOrDefault(predicate);
クラスレベル(静的またはインスタンス)述語ビルダー:
return this.items.FirstOrDefault(classLevelBuilder(day));
外部で定義され、メソッドパラメータとして提供される述語
public TItem GetItem(Func<TItem, bool> predicate)
{
return this.items.FirstOrDefault(predicate);
}
このメソッドを実行するとき、私は2つのアプローチも取りました。
for
ループ内のメソッド呼び出しで直接提供される述語:
for (int i = 0; i < 1000000; i++)
{
linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay()));
}
for
ループの外側で定義された述語ビルダー:
Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d);
for (int i = 0; i < 1000000; i++)
{
linqLookup.GetItem(builder(GetRandomDay()));
}
反復クラスを使用する場合の比較のために、それは約かかります。 770msランダムに生成された範囲で100万回のルックアップを実行します。
for
ループの外側で定義された述語ビルダー:885msfor
ループ内で定義された述語:1525msしたがって、外部から頻繁に呼び出されるメソッドで述語を使用する場合は常に、ビルダーを定義して実行してください。これにより、最良の結果が得られます。
これについて私が最も驚いたのは、デリゲート(または述語)にこれほど多くの時間がかかる可能性があることです。
Gabeの答え に加えて、GetPointData
への呼び出しごとにデリゲートを再構築するコストが原因であるように見える違いを確認できます。
GetPointData
クラスのIterationRangeLookupSingle
メソッドに1行追加すると、LinqRangeLookupSingle
と同じクロールペースまで遅くなります。それを試してみてください:
// in IterationRangeLookupSingle<TItem, TKey>
public TItem GetPointData(DateTime point)
{
// just a single line, this delegate is never used
Func<TItem, bool> dummy = i => i.IsWithinRange(point);
// the rest of the method remains exactly the same as before
// ...
}
(コンパイラやジッターが上記で追加した余分なデリゲートを無視できない理由はわかりません。明らかに、デリゲートは必要ですLinqRangeLookupSingle
クラスで。)
考えられる回避策の1つは、LinqRangeLookupSingle
で述語を作成して、point
が引数として渡されるようにすることです。つまり、デリゲートを作成する必要があるのは1回だけであり、GetPointData
メソッドが呼び出されるたびではありません。たとえば、次の変更によりLINQバージョンが高速化され、foreach
バージョンとほぼ同等になります。
// in LinqRangeLookupSingle<TItem, TKey>
public TItem GetPointData(DateTime point)
{
Func<DateTime, Func<TItem, bool>> builder = x => y => y.IsWithinRange(x);
Func<TItem, bool> predicate = builder(point);
return this.items.FirstOrDefault(predicate);
}
ループ内のデリゲートの生成(特にメソッド呼び出しに対する非自明なループ)によって時間が追加される可能性があるため、LINQの表示が遅くなることがあります。代わりに、Finderをクラスから移動して、より汎用的にすることを検討することをお勧めします(キーセレクターが構築中のように)。
public class LinqLookup<TItem, TKey>
{
private IList<Item> items = null;
public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
{
this.items = items.OrderByDescending(keySelector).ToList();
}
public TItem GetItem(Func<TItem, TKey> selector)
{
return this.items.FirstOrDefault(selector);
}
}
反復コードでラムダを使用しないため、ループを通過するたびにデリゲートを作成する必要があるため、これは少し異なる可能性があります。通常、この時間は毎日のコーディングにとって重要ではなく、デリゲートを呼び出す時間は他のメソッド呼び出しよりも高くはありません。タイトなループでデリゲートを作成するだけで、少し余分な時間が追加される可能性があります。
この場合、デリゲートはクラスに対して変更されないため、ループしているコードの外部でデリゲートを作成でき、より効率的になります。
更新:
実際、最適化を行わなくても、自分のマシンでリリースモードでコンパイルしても、5倍の違いは見られません。 Item
フィールドしかないDateTime
で1,000,000回のルックアップを実行し、リストに5,000個のアイテムが含まれています。もちろん、私のデータなどは異なりますが、デリゲートを抽象化すると、実際には時間が非常に近いことがわかります。
反復:14279ミリ秒、0.014279ミリ秒/呼び出し
linq w opt:17400ミリ秒、0.0174ミリ秒/呼び出し
これらの時差は非常にマイナーであり、LINQを使用することで読みやすさと保守性を向上させる価値があります。ただし、5倍の違いは見られません。そのため、テストハーネスには見られないものがあると思います。
次のようなループがあると仮定します。
for (int counter = 0; counter < 1000000; counter++)
{
// execute this 1M times and time it
DateTime day = GetRandomDay();
items.FirstOrDefault(i => i.IsWithinRange(day));
}
このループは、i.IsWithinRange
呼び出しがday
にアクセスするために1,000,000個のラムダオブジェクトを作成します。ラムダが作成されるたびに、i.IsWithinRange
を呼び出すデリゲートが平均1,000,000 * items.Length
/2回呼び出されます。これらの要素は両方ともforeach
ループには存在しません。そのため、明示的なループの方が高速です。