web-dev-qa-db-ja.com

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

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

すべてのシングルパス操作(SelectWhereCount、_Take/Skip_、_Any/All_など)がOになることは明らかです(n)、シーケンスを1回歩くだけでよいため。これでも怠の対象となります。

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

OrderByにはソートが必要なので、おそらくO(n log n)を見ていることになります。ソート済みの場合はどうなりますか? OrderBy().ThenBy()と言って、両方に同じキーを提供したらどうでしょうか。

ソートまたはハッシュを使用して、GroupBy(およびJoin)を確認できました。どっち?

Containsは、O(n)ではListになりますが、O(1)ではHashSet-LINQは基礎となるコンテナをチェックして、速度を上げることができるかどうかを確認しますか?

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

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

108
tzaman

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

  • ElementAtSkipLastLastOrDefaultなどのインデックス付きアクセスを使用する拡張メソッドは、基になる型がIList<T>を実装しているかどうかを確認します。そのため、O(N)の代わりにO(1)アクセスを取得します。

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

  • DistinctGroupByJoin、および集合集合メソッド(UnionIntersectおよびExceptも信じています。 )ハッシュを使用するため、O(N²)ではなくO(N)に近い値にする必要があります。

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

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

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

107
Aaronaught

あなたが本当に頼りにすることができるのは、Enumerableメソッドが一般的な場合によく書かれており、素朴なアルゴリズムを使用しないということです。おそらく実際に使用されているアルゴリズムを説明するサードパーティのもの(ブログなど)がありますが、これらはSTLアルゴリズムがそうであるという意味で公式または保証されていません。

説明のために、System.CoreからのEnumerable.Countの反映されたソースコード(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;
    }
}

ご覧のとおり、すべての要素を単純に列挙するという単純な解決策を回避するための努力が必要です。

8
Marcelo Cantos

列挙がIListの場合、.Count()が_.Count_を返すことは長い間知っていました。

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

.Intersect()(私のコメント)の逆コンパイルされた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)です
  • 実装does n'tコレクションが有効な場合already areセット。 (使用される_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

私はちょうどリフレクターを壊し、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
ChaosPandion

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

一般的に、希望する種類のパフォーマンス保証が見つかるとは思いませんが、linq演算子で特定のパフォーマンスの問題が発生した場合、特定のコレクションに対していつでも再実装できます。また、Linqをオブジェクトに拡張してこれらの種類のパフォーマンス保証を追加する多くのブログと拡張性プロジェクトがあります。 Indexed LINQ を確認してください。これにより、パフォーマンスが向上するように演算子セットが拡張および追加されます。

3
luke