4

私は現在作成しているアプリケーションでLINQクエリを多用していますが、私が遭遇し続ける状況の1つは、LINQクエリの結果をさらに処理するためにリストに変換する必要があることです(私には理由があります)リストが欲しい)。

私はこれを繰り返し使用しているので、非効率性がある場合にこのリスト変換で何が起こるかをよりよく理解したいと思います。したがって、次のような行行を実行するとします。

var matches = (from x in list1 join y in list2 on x equals y select x).ToList();

質問:

  1. 新しいリストの作成と、クエリから返されたEnumerableの要素への参照を含むその母集団以外に、ここでオーバーヘッドはありますか?

  2. これは非効率だと思いますか?

  3. この状況で変換の必要性を回避するために、LINQクエリを取得してリストを直接生成する方法はありますか?

4

5 に答える 5

5

さて、それはデータのコピーを作成します。それは非効率的かもしれません-しかしそれは何が起こっているかに依存します。最後にが必要な場合は、通常、得られるのと同じくらい効率的になります。唯一の例外は、変換を行うだけで、ソースがすでにリストになっている場合です。最初に適切なサイズのバッキング配列を作成できるため、使用する方が効率的です。List<T>List<T>ConvertAll

データをストリーミングするだけでよい場合(たとえば、データをストリーミングするだけforeachで、元のデータソースに影響を与えないアクションを実行する場合)、呼び出しToListは間違いなく非効率の潜在的な原因になります。これにより、全体が強制的にlist1評価されます。これが遅延評価されたシーケンス(たとえば、「乱数ジェネレーターからの最初の1,000,000個の値」)の場合、それは適切ではありません。結合を実行しているときは、シーケンスから最初の値を取得しようとするとすぐlist2に評価されることに注意してください(リストにデータを入力するためかどうかは関係ありません)。

私のEdulinqの投稿ToListを読んで、バックグラウンドで何が起こっているのか(少なくとも1つの可能な実装で)を確認することをお勧めします。

于 2012-07-24T18:15:37.350 に答える
1
  1. あなたがすでに言及したもの以外に、他のオーバーヘッドはありません。

  2. はい、と思いますが、具体的なアプリケーション シナリオによって異なります。ところで、一般的に、追加の呼び出しは避けたほうがよいでしょう。(これは明らかだと思います)。

  3. そうではないと思います。はデータのシーケンスを返しますが、これは潜在的に無限のシーケンスである可能LINQ queryがありますあなたに変換すると、 initまたはstreamを持つことができないインデックスアクセスの可能性もあり、finitになりますList<T>

提案: が必要な状況は避けてくださいList<T>。ちなみに、必要な場合は、現時点で必要なだけ少ないデータを内部にプッシュしてください。

お役に立てれば。

于 2012-07-24T18:18:07.890 に答える
1

前述の内容に加えて、結合しようとしている最初の 2 つのリストがすでに非常に大きい場合、3 番目のリストを作成する (2 つの「交差」を作成する) と、メモリ不足エラーが発生する可能性があります。LINQ ステートメントの結果を繰り返すだけで、メモリ使用量が大幅に削減されます。

于 2012-07-24T18:25:04.070 に答える
0
  1. オーバーヘッドのほとんどは、dbへの接続
    、アダプタへのデータの取得など、リストの作成前に発生します。varタイプの場合、.NETはデータタイプ/構造を決定する必要があります...

  2. 効率は非常に相対的な用語です。SQLに精通していないプログラマーにとっては、1で詳述されているオーバーヘッドを(古いADOと比較して)効率的かつ迅速に開発できます。

  3. 一方、LINQはデータベース自体からプロシージャを呼び出すことができます。これはすでに高速です。次のテストに進むことをお勧めします。

    • 最大量のデータでプログラムを実行し、時間を測定します。
    • いくつかのdbプロシージャを使用して、データをファイル(XML、CSVなど)にエクスポートし、そのファイルからリストを作成して時間を測定します。次に、違いが重要であるかどうかを確認できます。ただし、2番目の方法はプログラマーにとって効率が悪くなりますが、実行時間を短縮できます。
于 2012-07-24T19:00:10.543 に答える
0

Enumerable.ToList(source)は、本質的に単なる呼び出しnew List(source)です。

このコンストラクターは、 source が であるかどうかをテストしICollection<T>、そうである場合は適切なサイズの配列を割り当てます。それ以外の場合、つまりソースが LINQ クエリであるほとんどの場合、デフォルトの初期容量 (4 項目) で配列を割り当て、必要に応じて容量を 2 倍にして拡張します。容量が 2 倍になるたびに、新しいアレイが割り当てられ、古いアレイが新しいアレイにコピーされます。

これにより、リストに多数のアイテムが含まれる場合 (おそらく少なくとも数千個) にオーバーヘッドが発生する可能性があります。リストが 85 KB を超えるとすぐにオーバーヘッドが大きくなる可能性があります。これは、圧縮されず、メモリの断片化が発生する可能性があるラージ オブジェクト ヒープにリストが割り当てられるためです。リスト内の配列を参照していることに注意してください。が参照型の場合T、その配列には参照のみが含まれ、実際のオブジェクトは含まれません。これらのオブジェクトは、85 KB の制限にはカウントされません。

シーケンスのサイズを正確に見積もることができれば、このオーバーヘッドの一部を取り除くことができます (少し過小評価するよりも、少し過大評価する方が適切です)。たとえば、.Select()を実装するものに対してのみ演算子を実行してICollection<T>いる場合、出力リストのサイズがわかります。

そのような場合、この拡張メソッドはこのオーバーヘッドを削減します:

public static List<T> ToList<T>(this IEnumerable<T> source, int initialCapacity)
{
    // parameter validation ommited for brevity

    var result = new List<T>(initialCapacity);

    foreach (T item in source)
    {
        result.Add(item);
    }

    return result;
}

場合によっては、作成したリストが、以前の実行などですでに存在していたリストを置き換えるだけになることがあります。そのような場合、古いリストを再利用すると、かなりの数のメモリ割り当てを回避できます。ただし、その古いリストに同時にアクセスできない場合にのみ機能します。新しいリストが通常古いリストよりも大幅に小さい場合は、そうしません。その場合は、次の拡張メソッドを使用できます。

public static void CopyToList<T>(this IEnumerable<T> source, List<T> destination)
{
    // parameter validation ommited for brevity

    destination.Clear();

    foreach (T item in source)
    {
        destination.Add(item);
    }
}

そうは言っても、私.ToList()は非効率的だと思いますか?いいえ、メモリがあり、リストを繰り返し使用する場合は、リストにランダムにインデックスを付けたり、複数回繰り返したりします。

特定の例に戻ります。

var matches = (from x in list1 join y in list2 on x equals y select x).ToList(); 

他の方法でこれを行う方が効率的な場合があります。たとえば、次のようになります

var matches = list1.Intersect(list2).ToList();

list1 と list2 に重複が含まれていない場合は同じ結果が得られ、list2 が小さい場合は非常に効率的です。

ただし、いつものように、実際に知る唯一の方法は、典型的なワークロードを使用して測定することです。

于 2012-07-24T18:59:33.693 に答える