2013-06-05 11 views
24

私は.NETのEntity Frameworkで初めて作業しており、モデルから情報を取得するためにLINQクエリを作成しています。私は初めから良い習慣でプログラムを作りたいので、私はこれらの質問を書いてその結果を得る最善の方法を研究してきました。残念ながら、ブラウジングスタックExchangeで、私は延期/即時実行は、LINQでどのように機能するかに相反する2つの説明に遭遇しているように見えました:"foreach"はLinqの実行を繰り返しますか?

  • のforeachループの各反復で実行されるクエリが発生します。

が質問Slow foreach() on a LINQ query - ToList() boosts performance immensely - why is this?で実証、含意はforeachのはかなり動作が遅く、繰り返しデータソースにクエリを評価しているとして「ToListメソッド()は」、すぐにクエリを評価するために呼び出される必要があることです。

もう1つの例は、質問Foreaching through grouped linq results is incredibly slow, any tips?です。受け入れられた回答は、クエリで "ToList()"を呼び出すとパフォーマンスが向上することを意味します。

で使用しても安全である、含意はforeachのが確立されるべき1つの列挙を引き起こすことがある、としません毎回データソースを照会します。

"foreachループ中の繰り返し実行"がパフォーマンス上の懸念の原因であることを多くの疑問が浮き彫りになりました。また、foreachがデータソースから単一のクエリを適切に取得し、両方の説明には妥当性があるようです。 「ToList()」仮説が間違っている場合(2013-06-05の午後1時51分現在のほとんどの現時点での回答が示唆しているように見える)、この誤解はどこから来たのでしょうか?これらの説明のうち正確なものとそうでないものがありますか、LINQクエリの評価方法が異なるさまざまな状況がありますか?

編集:私は以下の質問に答えるだけでなく、ループ中に複数のデータソースがヒットする可能性のある落とし穴https://softwareengineering.stackexchange.com/questions/178218/for-vs-foreach-vs-linq

+0

foreach内のクエリが実際に行っていることに依存すると思います。 – asawyer

+0

'foreach'は、パフォーマンス問題のために' IEnumerable'/'IQueryable'と一緒に使用されると、通常は原因です。 – Aron

+0

これについて適切に判断できるようにするには、非常に具体的な例が必要です。 'foreach'は' GetEnumerator'を一度呼び出すだけですが、 'foreach'ループを複数回実行すると' GetEnumerator'を複数回呼び出します... –

答えて

14

一般に、LINQは遅延実行を使用します。 First()FirstOrDefault()のようなメソッドを使用すると、すぐにクエリが実行されます。あなたが何かのようにするとき。

結果はストリーミング方式で1つずつ取得されます。イテレータがMoveNextを呼び出すたびに、投影が次のオブジェクトに適用されます。 Whereを持っていた場合は、最初にフィルタを適用してから投影します。

List<string> names = People.Select(x => x.Name).ToList(); 
foreach (string name in names) 

次に、これは無駄な操作だと思います。 ToList()は、クエリを強制的に実行してPeopleリストを列挙し、x => x.Name投影を適用します。その後、リストを再度列挙します。だから、あなたが(IEnumeraleではなく)リストにデータを持つ良い理由がなければ、CPUサイクルを浪費しているだけです。

一般的に言えば、foreachを使用して列挙しているコレクションのLINQクエリを使用すると、他の類似した実用的なオプションよりもパフォーマンスが低下することはありません。

LINQプロバイダーを実装しているユーザーは、マイクロソフトが提供するプロバイダーと同じように共通メソッドを機能させることを奨励されていますが、必須ではありません。 LINQをHTMLまたはLINQに書き込んで私の独自のデータフォーマットプロバイダに書き込む場合、この方法で動作する保証はありません。おそらく、データの性質上、即時実行が実用的な唯一の選択肢になります。

また、最終編集。このJon SkeetのC#In Depthに興味があるなら、非常に有益であり、素晴らしい読書です。私の答えは本書のいくつかのページを要約していますが(妥当な精度でうまくいけばうれしいですが)、LINQがどのように機能しているかについて詳しく知りたい場合は、見ておくのが良い場所です。

+0

本のおかげでありがとうございました。これは、私自身の手のひらの動きや、私が立ち上がっている記事の一般的な合意が何であるかを確認するようです。 – Mejwell

+1

列挙子で 'Current'を呼び出すときに投影法は* not *が適用されます。これは' MoveNext'を呼び出すときに適用されます。 'Current'は' MoveNext'への呼び出しで生成された値を取得しています。これは良いことです。つまり、 'MoveNext'への呼び出しの間に' Current'を2回以上使用することについて心配する必要はありません。 – Servy

+0

@Servy訂正ありがとうございました。あなたが好きなら投稿を編集してください。 – evanmcdonnal

0

エンティティがなくてもLINQを使用すると、遅延実行が有効になることがわかります。 実際のlinq式が評価されることを強制するだけです。 その意味で、linq式を使うたびに評価されます。

エンティティの場合、これはまだ同じですが、ここではもっと機能があります。 エンティティフレームワークが最初に式を見ると、彼はすでにこのクエリを実行したかどうかを調べます。そうでない場合は、データベースにアクセスしてデータを取得し、内部メモリモデルを設定してデータを返します。エンティティフレームワークがデータを事前に取得していると判断した場合は、データベースにアクセスせずに先に設定したメモリモデルを使用してデータを返します。

これはあなたの人生を楽にすることができますが、痛みを伴うこともあります。たとえば、linq式を使用してテーブルからすべてのレコードを要求するとします。エンティティ・フレームワークは、表からすべてのデータをロードします。後で同じlinq式を評価しても、レコードが削除または追加されても同じ結果が得られます。

エンティティフレームワークは複雑なものです。もちろん、独自のメモリモデルなどにある変更を考慮してクエリを再実行させる方法もあります。

Julia Lermanの「プログラミングエンティティフレームワーク」を読むことをお勧めします。それは、あなたが今持っているもののような多くの問題に取り組んでいます。

+0

'ToList()'が 'foreach()'よりも速くなければならない理由は全く正しいが説明はない。それはおそらく決してありません。 –

+0

私はforeachがToListよりも遅くなるとは思わない。 –

2

foreachは、単独でデータを1回だけ実行します。実際には、具体的にはです。 forループでは、先読みや後戻り、または索引の変更はできません。

ただし、コード内に複数のforeachがあり、すべて同じLINQクエリで動作している場合は、クエリが複数回実行されることがあります。しかし、これは完全にデータに依存しています。データベースクエリを表すLINQベースのIEnumerable/IQueryableを反復処理している場合は、毎回そのクエリが実行されます。 Listまたは他のオブジェクトコレクションを反復処理している場合は、毎回リストを実行しますが、データベースに繰り返しヒットすることはありません。

つまり、LINQのプロパティで、のforeachのプロパティではありません。

4

どのようにLinqクエリが使用されているかによって異なります。

var q = {some linq query here} 

while (true) 
{ 
    foreach(var item in q) 
    { 
    ... 
    } 
} 

上記のコードは、LINQクエリを複数回実行します。 foreachのためではなく、foreachが別のループの内側にあるので、foreach自体が複数回実行されます。

linqクエリのすべてのコンシューマがそれを「注意深く」使用し、上のネストループなどの間違いを避ける場合、linqクエリは不必要に複数回実行されるべきではありません。

ToList()を使用してメモリ内の結果セットにlinqクエリを減らすことが保証されていますが、私の意見では、ToList()ははるかに頻繁に使用されます。最も外側の消費者/列挙子が10行しか必要としなくても、結果セット全体(おそらく何百万行も)をメモリに格納してキャッシュに入れるため、大量のデータが含まれているときは常にToList()が毒薬になります。非常に具体的な正当な理由がなく、データが決して大きくならない限り、ToList()を避けてください。

6

は、デリゲートが実行される場合、我々はそれゆえ、我々はLINQクエリをするたびに実行されて見ることができ、コンソール出力を参照するものとLinqPad

void Main() 
{ 
    var testList = Enumerable.Range(1,10); 
    var query = testList.Where(x => 
    { 
     Console.WriteLine(string.Format("Doing where on {0}", x)); 
     return x % 2 == 0; 
    }); 
    Console.WriteLine("First foreach starting"); 
    foreach(var i in query) 
    { 
     Console.WriteLine(string.Format("Foreached where on {0}", i)); 
    } 

    Console.WriteLine("First foreach ending"); 
    Console.WriteLine("Second foreach starting"); 
    foreach(var i in query) 
    { 
     Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i)); 
    } 
    Console.WriteLine("Second foreach ending"); 
} 

にするたびに、これを試してみてください。コンソールの出力を見ると、2番目のforeachループが "Doing where on"という文字列を表示し、実際には2番目のforeachの使用が実際には再びwhere句を実行することを示しています。 。

First foreach starting 
Doing where on 1 
Doing where on 2 
Foreached where on 2 
Doing where on 3 
Doing where on 4 
Foreached where on 4 
Doing where on 5 
Doing where on 6 
Foreached where on 6 
Doing where on 7 
Doing where on 8 
Foreached where on 8 
Doing where on 9 
Doing where on 10 
Foreached where on 10 
First foreach ending 
Second foreach starting 
Doing where on 1 
Doing where on 2 
Foreached where on 2 for the second time. 
Doing where on 3 
Doing where on 4 
Foreached where on 4 for the second time. 
Doing where on 5 
Doing where on 6 
Foreached where on 6 for the second time. 
Doing where on 7 
Doing where on 8 
Foreached where on 8 for the second time. 
Doing where on 9 
Doing where on 10 
Foreached where on 10 for the second time. 
Second foreach ending 
+3

あなたの答えにこれがどのように示されているか説明してください。 – Bobson

+0

デリゲートが実行されるたびにコンソール出力が表示されるので、毎回Linqクエリが実行されているのがわかります。コンソールの出力を見ると、2番目のforeachループが "Doing where on"という文字列を表示し、実際には2番目のforeachの使用が実際には再びwhere句を実行することを示しています。 。 – Aron

+1

"これを試してください"というより、答えに*を含める必要があります。 – Bobson

1

違いは基本的なタイプにあります。 LINQはIEnumerable(またはIQueryable)の上に構築されるため、同じLINQ演算子は完全に異なるパフォーマンス特性を持つ可能性があります。

リストは常に迅速に対応することができますが、リストを作成するには前もって努力が必要です。

イテレータもIEnumerableであり、「次の」項目をフェッチするたびに任意のアルゴリズムを使用できます。実際に完全なアイテムセットを通過する必要がない場合は、これは速くなります。

IEnumerableをリストにするには、ToList()を呼び出して結果のリストをローカル変数に格納します。

  • 遅延実行には依存しません。
  • あなたは、セット全体よりも多くの項目をアクセスする必要があります。
  • すべてのアイテムを取得して保存するための初期費用を支払うことができます。
  • 時には
2

クエリは、あなたのコード内で複数回アクセスされている場合には、ToList()またはToArray()を使用してLINQクエリ「キャッシュ」には良い考えかもしれません

ただし、「キャッシング」すると、まだforeachが呼び出されます。

だから、私のための基本的なルールは次のとおりです。

  • 問合せは、単純に1 foreachで使用(およびそれのthats)されている場合 - クエリがで使用されている場合、私は、クエリ
  • をキャッシュしません。コード内のいくつかの他の場所でforeach - その後、私はToList/ToArray
+1

"バッファ"、 "熱心な実行"、または使用したように、 "キャッシュ"は "シリアライズ"よりも良い用語でしょうか? – BACON

+0

@BACONはい、あなたは正しいです、answrerを更新しました – jazzcat

-1

を使用してVARでそれをキャッシュすることがLINQ statemを実行しますあなたが.ToList()を行うかどうかにかかわらず、と同じ回数。私は、コンソールに、ここでカラー出力を持つ例があります。

コードで何が起こる(一番下のコードを参照してください):

  • は100個のint型(0-99)のリストを作成します。
  • リストからすべてのintを出力し、次に2つの*をコンソールに赤色で出力し、それが偶数の場合はintを返すLINQ文を作成します。
  • queryでforeachを実行し、すべての偶数を緑色で印刷します。
  • query.ToList()でforeachを実行し、すべての偶数を緑色で印刷します。

以下の出力からわかるように、コンソールに書き込まれるintの数は同じです。つまり、LINQ文が同じ回数実行されます。

が実行されたときの違いです。ご覧のとおり、.ToList()が呼び出されていないというクエリでforeachを実行すると、リストとLINQステートメントから返されたIEnumerableオブジェクトが同時に列挙されます。

リストを最初にキャッシュすると、リストは別々に列挙されますが、同じ時間が残ります。あなたはLINQ文を定義した後にリストが変更された場合、それが実行されると、LINQ文が(例えば.ToList()で)修正リスト上で動作しますので、

違いは、を理解することが非常に重要です。ただし、LINQステートメント(.ToList())の実行を強制し、その後にリストを変更すると、LINQステートメントは変更されたリストでは機能しません。ここで

は出力です: LINQ Deferred Execution output

ここに私のコードです:実行時間程度

// Main method: 
static void Main(string[] args) 
{ 
    IEnumerable<int> ints = Enumerable.Range(0, 100); 

    var query = ints.Where(x => 
    { 
     Console.ForegroundColor = ConsoleColor.Red; 
     Console.Write($"{x}**, "); 
     return x % 2 == 0; 
    }); 

    DoForeach(query, "query"); 
    DoForeach(query, "query.ToList()"); 

    Console.ForegroundColor = ConsoleColor.White; 
} 

// DoForeach method: 
private static void DoForeach(IEnumerable<int> collection, string collectionName) 
{ 
    Console.ForegroundColor = ConsoleColor.Yellow; 
    Console.WriteLine("\n--- {0} FOREACH BEGIN: ---", collectionName); 

    if (collectionName.Contains("query.ToList()")) 
     collection = collection.ToList(); 

    foreach (var item in collection) 
    { 
     Console.ForegroundColor = ConsoleColor.Green; 
     Console.Write($"{item}, "); 
    } 

    Console.ForegroundColor = ConsoleColor.Yellow; 
    Console.WriteLine("\n--- {0} FOREACH END ---", collectionName); 
} 

注:私は(ただし、ここでそれを投稿するには十分ではない)いくつかのタイミングテストを行なったし、私はdidnのいずれの方法でも一貫性が他の方法より速いことがわかります(タイミングで.ToList()の実行を含む)。大規模なコレクションでは、最初にコレクションをキャッシュしてからそれを反復するのが少し速いように見えましたが、私のテストでは決定的な結論は出ませんでした。