LINQメソッドの実行時の複雑さ(Big-O)にはどのような保証がありますか?


120

私は最近LINQをかなり使い始めましたが、どのLINQメソッドの実行時の複雑さについてもまったく触れていません。明らかに、ここには多くの要素が関係しているので、議論をプレーンなIEnumerableLINQ-to-Objectsプロバイダーに限定しましょう。さらに、Funcセレクター/ミューテーター/などとして渡されたものはすべて安価なO(1)操作であると想定しましょう。

これは、すべてのシングルパス動作することを明らかに思える(SelectWhereCountTake/SkipAny/All、など)彼らは一度だけシーケンスを歩く必要があるので、O(n)となります。でもこれは怠惰の対象です。

より複雑な操作では物事は危険です。セットのような演算子(UnionDistinctExcept、など)を使用して作業GetHashCodeデフォルトでは(私の知る限り)、彼らが、一般的には、だけでなく、これらの操作のO(n)を作り、内部ハッシュ・テーブルを使用していると仮定するのが妥当と思われるので。を使用するバージョンはIEqualityComparerどうですか?

OrderByソートが必要になるので、おそらくO(n log n)を調べています。すでに並べ替えられている場合はどうなりますか?私が言っOrderBy().ThenBy()て両方に同じキーを提供したらどうですか?

並べ替えまたはハッシュのいずれかを使用してGroupBy(およびJoin)を表示できました。どっち?

ContainsはO(n)ですが、A ListはO(1)ですHashSet-LINQは基礎となるコンテナーをチェックしてスピードアップできるかどうかを確認しますか?

そして本当の質問-これまでのところ、私は操作が高性能であることを信じてそれを取っています。しかし、それを利用することはできますか?たとえば、STLコンテナは、すべての操作の複雑さを明確に指定します。.NETライブラリ仕様のLINQパフォーマンスについて同様の保証はありますか?

その他の質問(コメントへの回答):
オーバーヘッドについてはあまり考えていませんでしたが、単純なLinq-to-Objectsについてはそれほど多くあるとは思いませんでした。CodingHorrorの投稿はLinq-to-SQLについて話しており、クエリを解析してSQLを作成するとコストが追加されることを理解できます。オブジェクトプロバイダーにも同様のコストがありますか?もしそうなら、宣言構文または関数構文を使用している場合とは異なりますか?


私は本当にあなたの質問に答えることはできませんが、一般に、パフォーマンスの大部分はコア機能と比較して「オーバーヘッド」になるとコメントしたいと思います。もちろん、非常に大きなデータセット(1万個を超えるアイテム)がある場合はそうではないので、知りたい場合は知りたいです。
アンリ

2
再:「宣言構文または関数構文を使用している場合は異なりますか?」-コンパイラーは、宣言構文を関数構文に変換して、同じになるようにします。
John Rasch、

「STLコンテナーは、すべての操作の複雑さを明確に指定します」.NETコンテナーは、すべての操作の複雑さも明確に指定します。Linq拡張機能は、STLコンテナーではなく、STLアルゴリズムに似ています。STLアルゴリズムをSTLコンテナーに適用する場合と同様に、結果の複雑さを適切に分析するには、Linq拡張機能の複雑さと.NETコンテナー操作の複雑さを組み合わせる必要があります。これには、Aaronaughtの回答が述べているように、テンプレートの特殊化の説明が含まれます。
Timbo

根本的な問題は、開発者がコードのパフォーマンスに依存している場合、開発者が文書化されていない動作に依存する必要があるため、IList <T>の最適化がユーティリティの制限になることをMicrosoftがそれほど懸念しなかった理由です。
エドワードブレイ

結果セットリストのAsParallel()。〜O(1)<O(n)
レイテンシ

回答:


121

保証はほとんどありませんが、いくつかの最適化があります。

  • などのインデックス付きアクセスを使用して拡張メソッド、ElementAtSkipLastまたはLastOrDefault、かどうか根本的なタイプの実装を確認するためにチェックしますIList<T>ので、あなたはO(N)のO(1)アクセスの代わりに取得すること。

  • このCountメソッドはICollection実装をチェックするため、この操作はO(N)ではなくO(1)になります。

  • DistinctGroupBy JoinおよびIセット凝集方法も信じ(UnionIntersectそしてExceptそれらはO(N)の代わりに、O(N²)に近くなければならないので、使用するハッシュを)。

  • ContainsICollection実装をチェックするため、基になるコレクションものようにO(1)の場合はO(1)になりHashSet<T>ますが、これは実際のデータ構造に依存し、保証されません。ハッシュセットはContainsメソッドをオーバーライドするため、O(1)になります。

  • OrderBy メソッドは安定したクイックソートを使用するため、O(N log N)の平均ケースです。

私は、組み込み拡張メソッドのすべてではないにしても、ほとんどをカバーしていると思います。実際にパフォーマンスが保証されることはほとんどありません。Linq自体は効率的なデータ構造を利用しようとしますが、潜在的に非効率的なコードを書くための無料のパスではありません。


どの程度IEqualityComparerのオーバーロード?
tzaman

@tzaman:それらについてはどうですか?本当に非効率なカスタムを使用しない限りIEqualityComparer、漸近的な複雑さに影響を与える理由はありません。
アーロンノート

1
ああ、そうです。だけでなく、EqualityComparer実装GetHashCodeを実現していませんでしたEquals。もちろん、それは完全に理にかなっています。
tzaman

2
@imgen:ループ結合はO(N * M)で、関係のないセットに対してO(N²)に一般化されます。LinqはO(N + M)であるハッシュ結合を使用します。これはO(N)に一般化されます。これは中途半端なハッシュ関数を前提としていますが、.NETでめちゃくちゃにするのは困難です。
アーロンノート、2014年

1
ですOrderby().ThenBy()まだN logNあるいはそれである(N logN) ^2か、そのような何か?
M.kazem Akhgary 2015

10

列挙型がである場合に.Count()返されることは以前から知っていまし.CountIList

しかし、私はいつも、Set操作の実行時の複雑さについて少し疲れました:.Intersect().Except().Union()

.Intersect()(コメントmine)の逆コンパイルされたBCL(.NET 4.0 / 4.5)実装は次のとおりです。

private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)                    // O(M)
    set.Add(source);                                    // O(1)

  foreach (TSource source in first)                     // O(N)
  {
    if (set.Remove(source))                             // O(1)
      yield return source;
  }
}

結論:

  • パフォーマンスはO(M + N)
  • コレクションがすでに設定されている場合、実装利用しません。(中古品も一致する必要があるため、必ずしも単純ではない場合があります。)IEqualityComparer<T>

完全を期すために.Union()、およびの実装を示し.Except()ます。

ネタバレ注意:彼らもO(N + M)の 複雑さを持っています。

private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
  foreach (TSource source in second)
  {
    if (set.Add(source))
      yield return source;
  }
}


private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)
    set.Add(source);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
}

8

Enumerableメソッドは一般的なケース向けに適切に記述されており、単純なアルゴリズムを使用しないというだけで、本当に頼りになります。実際に使用されているアルゴリズムを説明しているサードパーティのもの(ブログなど)がおそらくあるでしょうが、これらは公式ではなく、STLアルゴリズムがそうであるという意味で保証されていません。

説明のためにEnumerable.Count、System.Coreからの反映されたソースコード(ILSpy提供)を次に示します。

// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    checked
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        ICollection<TSource> collection = source as ICollection<TSource>;
        if (collection != null)
        {
            return collection.Count;
        }
        ICollection collection2 = source as ICollection;
        if (collection2 != null)
        {
            return collection2.Count;
        }
        int num = 0;
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                num++;
            }
        }
        return num;
    }
}

ご覧のように、単純にすべての要素を列挙するという単純な解決策を回避するための努力がなされています。


IEnnumerableである場合にオブジェクト全体を反復処理してCount()を取得するのは、私には非常にナイーブに思えます...
Zonko

4
@ゾンコ:あなたの言い分がわかりません。Enumerable.Count明らかな代替手段がない限り、それが繰り返されないことを示すために私の回答を修正しました。どのようにしてそれを単純にしないのですか?
Marcelo Cantos、2011年

まあ、はい、メソッドは、ソースを与えられた最も効率的な方法で実装されています。ただし、最も効率的な方法は単純なアルゴリズムである場合があり、呼び出しの実際の複雑さを隠すため、linqを使用するときは注意が必要です。操作しているオブジェクトの基本的な構造に慣れていない場合は、ニーズに合わせて間違ったメソッドを簡単に使用できます。
Zonko

@MarceloCantosなぜ配列が処理されないのですか?ElementAtOrDefaultメソッドreferencesource.microsoft.com/#System.Core/System/Linq/…
Freshblood

@Freshblood彼らはそうです。(配列はICollectionを実装します。)ただし、ElementAtOrDefaultについてはわかりません。配列にはICollection <T>も実装されていると思いますが、最近の.Netはかなり錆びています。
Marcelo Cantos 2015

3

私はリフレクターを壊しただけで、Contains呼び出されたときに基礎となる型をチェックします。

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> is2 = source as ICollection<TSource>;
    if (is2 != null)
    {
        return is2.Contains(value);
    }
    return source.Contains<TSource>(value, null);
}

3

正解は「場合によります」です。基になるIEnumerableがどのタイプかによって異なります。一部のコレクション(ICollectionまたはIListを実装するコレクションなど)には、使用される特別なコードパスがあることを知っていますが、実際の実装は特別なことを保証するものではありません。たとえば、ElementAt()には、Count()と同様に、インデックス可能なコレクションの特別なケースがあることを知っています。ただし、一般的には、最悪の場合のO(n)パフォーマンスを想定する必要があります。

一般的に、必要な種類のパフォーマンス保証が見つかるとは思いませんが、linq演算子で特定のパフォーマンスの問題が発生した場合は、常に特定のコレクションに再実装するだけで済みます。また、Linqをオブジェクトに拡張してこれらの種類のパフォーマンス保証を追加するブログや拡張性プロジェクトも数多くあります。 演算子セットに拡張および追加されたインデックス付きLINQをチェックして、パフォーマンス上の利点を増やしてください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.