[C#] LINQ 메서드의 런타임 복잡성 (Big-O)에 대해 어떤 보장이 있습니까?

저는 최근에 LINQ를 꽤 많이 사용하기 시작했으며 LINQ 메서드에 대한 런타임 복잡성에 대한 언급을 실제로 보지 못했습니다. 분명히 여기에는 많은 요소가 작용하므로 일반 IEnumerableLINQ-to-Objects 공급자에 대한 논의를 제한하겠습니다 . 또한 Func선택자 / 변이 자 등으로 전달 된 모든 것이 저렴한 O (1) 연산 이라고 가정 해 봅시다 .

그것은 분명한 것 같다 모든 싱글 패스 (single-pass) 작업 (즉 Select, Where, Count, Take/Skip,Any/All 그들이 한 번만 순서를 걸어야하기 때문에, 등), O (n)이 될 것입니다; 이것조차도 게으름의 대상입니다.

더 복잡한 작업의 경우 상황이 더 어둡습니다. 세트 같은 사업자 ( Union, Distinct, Except, 등)를 사용하여 작업 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을 만들면 비용이 추가되는 것을 이해할 수 있습니다. Objects 공급자에게도 비슷한 비용이 있습니까? 그렇다면 선언적 또는 기능적 구문을 사용하는 경우 다른가요?



답변

보장은 거의 없지만 몇 가지 최적화가 있습니다.

  • 같은 인덱스 액세스를 사용 확장 방법, ElementAt, Skip, Last또는 LastOrDefault, 여부 기본 타입의 구현을 확인합니다 IList<T>, 그래서 당신은 O (N)의 O (1) 접근 대신 얻을.

  • Count위한 검사 방법 ICollection의 구현되므로이 조작인지 O (1)이 아닌 O (N).

  • Distinct,, GroupBy Join그리고 집합 집계 방법 ( Union, IntersectExcept)도 해싱을 사용하므로 O (N²) 대신 O (N)에 가까워 야합니다.

  • Contains을 검사 ICollection구현은 그렇게 수도 기본 모음 등으로, 또한, O (1)의 경우 O (1) 일 수 HashSet<T>있지만, 인 실제 데이터 구조에 의존하지 않을 수 있습니다. 해시 세트는 Contains메서드를 재정의하므로 O (1)입니다.

  • OrderBy 메서드는 안정적인 퀵 정렬을 사용하므로 O (N log N) 평균 케이스입니다.

나는 그것이 모든 내장 확장 방법은 아니지만 대부분을 포함한다고 생각합니다. 실제로 성능 보장은 거의 없습니다. Linq 자체는 효율적인 데이터 구조를 활용하려고 시도하지만 잠재적으로 비효율적 인 코드를 작성하는 것은 자유 패스가 아닙니다.


답변

나는 그렇게 오래 알고 지낸 .Count()수익을 .Count열거가 인 경우IList .

하지만 설정 작업의 실행 시간 복잡도에 대한 지친 조금 언제나 : .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)
  • 컬렉션이 이미 설정된 경우 구현 이점을 얻지 못합니다 . (사용 된 것과 일치해야하기 때문에 반드시 간단하지 않을 수 있습니다 .)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;
  }
}


답변

실제로 활용할 수있는 것은 Enumerable 메서드가 일반적인 경우에 잘 작성되고 순진한 알고리즘을 사용하지 않는다는 것입니다. 실제로 사용중인 알고리즘을 설명하는 타사 자료 (블로그 등)가있을 수 있지만 STL 알고리즘이라는 의미에서 공식적이거나 보장되지는 않습니다.

다음은 Enumerable.CountSystem.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;
    }
}

보시다시피 모든 요소를 ​​단순히 열거하는 순진한 해결책을 피하기 위해 노력합니다.


답변

방금 리플렉터를 부수고 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);
}


답변

정답은 “상황에 따라 다름”입니다. 기본 IEnumerable이 어떤 유형인지에 따라 다릅니다. ICollection 또는 IList를 구현하는 컬렉션과 같은 일부 컬렉션에는 특수 코드 경로가 사용된다는 것을 알고 있지만 실제 구현은 특별한 작업을 보장하지 않습니다. 예를 들어, ElementAt ()에는 Count ()와 마찬가지로 인덱스 가능한 컬렉션에 대한 특별한 경우가 있다는 것을 알고 있습니다. 그러나 일반적으로 최악의 경우 O (n) 성능을 가정해야합니다.

일반적으로 원하는 성능 보장을 찾을 수 없다고 생각하지만 linq 연산자로 특정 성능 문제가 발생하면 항상 특정 컬렉션에 대해 다시 구현할 수 있습니다. 또한 Linq를 Objects로 확장하여 이러한 종류의 성능 보장을 추가하는 많은 블로그와 확장 성 프로젝트가 있습니다. 더 많은 성능 이점을 위해 연산자 집합을 확장하고 추가하는 Indexed LINQ 를 확인하십시오 .


답변